Compare commits

..

1 Commits

Author SHA1 Message Date
Géry Debongnie 46095b7a96 [ADD] playground: add monkey patching example
closes #312
2019-10-05 09:00:23 +02:00
328 changed files with 91554 additions and 66728 deletions
-28
View File
@@ -1,28 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run check-formatting
- run: npm run build
+1 -9
View File
@@ -15,7 +15,6 @@ yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
#ide's
.vscode
@@ -24,11 +23,4 @@ yarn.lock
node_modules
# Extras temp file
/tools/owl.js
release-notes.md
.rpt2_cache
# useful in some cases
/temp
/tools/owl.js
-771
View File
@@ -1,771 +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.
## From Owl 1.x to Owl 2.0
All changes are documented here in no particular order.
**Components**
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- breaking: component.el is removed ([details](#9-componentel-is-removed))
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
- 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: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
**Templates**
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
- new: the `this` variable in template expressions is now bound to the component
**Reactivity**
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
**Slots**
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
- slots capabilities have been improved ([doc](doc/reference/slots.md))
- params can be give to slot content (to pass information from slot owner to slot user)
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
- slots can define scopes (to pass information from slot user to slot owner)
**Portal**
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-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))
**Miscellaneous**
- improved performance
- much simpler code
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
- 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: `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: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
- breaking: `browser` object has been removed ([details](#39-browser-object-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
});
}
}
```
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
### 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.
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
### 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, so the same rationale applies: in a way,
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
every value used by the component.
Migration code: remove the `shouldUpdate` methods, and it should work as well
as before.
### 9. component.el is removed
This comes from the fact that Owl 2 supports fragments (arbitrary content).
Migration: if one need a reference to the root htmlelement of a template, it is
suggested to simply add a `ref` on it, and access the reference as needed.
Another way to get access to a root node or html element is the hook `useRoots`
Documentation:
- [Refs](doc/reference/refs.md)
- [useRoots](doc/reference/hooks.md#useroots)
### 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`. Remember that you the root component
can have multiple roots
Documentation:
- [Fragments](doc/reference/templates.md#fragments)
- [Mounting a component](doc/reference/app.md#mount-helper)
### 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.
Documentation: [Environment](doc/reference/environment.md)
### 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.
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
### 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(...)`
Documentation: [EventBus](doc/reference/utils.md#eventbus)
### 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. `useChildSubEnv` (only applies to child components)
In Owl, a call to `useSubEnv` would define a new environment for the children
AND the component. It is very useful, but in some cases, one only need to update
the children component environment. This can now be done with a new hook:
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
### 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.
Documentation: [Environment](doc/reference/environment.md)
### 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>
```
Documentation: [Event Handling](doc/reference/event_handling.md)
### 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
```
Documentation: [Fragments](doc/reference/templates.md#fragments)
### 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;
setup () {
Object.assign(this, context);
}
}
const div = document.createElement('div');
document.body.appendChild(div);
const app = new App(C);
await app.mount(div);
const result = div.innerHTML;
app.destroy();
div.remove();
return result;
}
```
The function above works for most cases, but is asynchronous. An alternative
function could look like this:
```js
const { App, blockDom } = owl;
const app = new App(Component); // act as a template repository
function renderToString(template, context = {}) {
app.addTemplate(template, template, { allowDuplicate: true });
const templateFn = app.getTemplate(template);
const bdom = templateFn(context, {});
const div = document.createElement('div')
blockDom.mount(bdom, div);
return div.innerHTML;
}
```
This is a synchronous function, so it will not work with components, but it should
be useful for most simple templates.
Also note that these two examples do not translate their templates. To do that,
they need to be modified to pass the proper translate function to the `App`
configuration.
### 33. Portal are now defined with `t-portal`
Before Owl 2, one could use the `Portal` component by importing it and using it.
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
```xml
<div>
some content
<span t-portal="'body'">
portalled content
</span>
<div>
```
Rationale: it makes it slightly simpler to use (just need the directive, instead
of having to import and use a sub component), it makes the implementation slightly
simpler as well. Also, it prevents subclassing the Portal component, which could
be dangerous, since it is really doing weird stuff under the hood, and could
easily be broken inadvertendly.
### 34. `debounce` utility function has been removed
Rationale: it did not really help that much, is available as utility function
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
it does best.
### 35. `render` method does not return a promise anymore
Rationale: using the `render` method directly and waiting for it to complete
was slightly un-declarative. Also, it can be done using the lifecycle hooks
any way.
Migration: if necessary, one can use the lifecycle hooks to execute code after
the next mounted/patched operation.
### 36. `catchError` method is replaced by `onError` hook
The `catchError` method was used to provide a way to components to handle errors
occurring during the component lifecycle. This has been replaced by a `onError`
hook, with a similar API.
Rationale: `catchError` felt a little big awkward, when most of the way we
interact with componentss is via hooks. Using hooks felt more natural and
consistent.
Migration: mostly replace all `catchError` methods by `onError` hooks in the
`setup` method.
Documentation: [Error Handling](doc/reference/error_handling.md)
## 37. Support for inline css (`css` tag and static `style`) has been removed
Rationale: Owl tries to focus on what it does best, and supporting inline css
was not a priority. It used to support some simplified scss language, but it
was feared that it would cause more trouble than it was worth. Also, it seems
like it can be done in userspace.
Migration: it seems possible to implement an equivalent solution using hooks. A
simple implementation could look like this:
```js
let cache = {};
function useStyle(css) {
if (!css in cache) {
const sheet = document.createElement("style");
sheet.innerHTML = css;
cache[css] = sheet;
document.head.appendChild(sheet);
}
}
```
## 38. `t-raw` directive has been removed (replaced by `t-out`)
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
It is replaced by the `t-out` directive, which is safer: it requires the data
to be marked explicitely as markup if it is to be inserted without escaping.
Otherwise, it will be escaped (just like `t-esc`).
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
to mark all the js values.
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
## 39. `browser` object has been removed
Rationale: the `browser` object caused more trouble than it was worth. Also, it
seems like this should be done in user space, not at the framework level.
Migration: code should just be adapted to either use another browser object,
or to use native browser function (and then, just mock them directly).
## 40. Rendering a component does not necessarily render child components
Before, if one had the following component tree:
```mermaid
graph TD;
A-->B;
A-->C;
```
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
if their props have changed.
Now, the question is what happens if the props have changed, but in a deeper way?
In that case, Owl will know, because each props are now reactive. So, if some
inner value read by `B` was changed, then only `B` will be updated.
Rationale: This was just not possible in Owl 1, but it now possible. This is
due to the rewriteof the underlying rendering engine and the reactivity
system. The goal is to have a big performance boost in large screen with many
components: now Owl only rerender what is strictly useful.
+213 -82
View File
@@ -1,128 +1,259 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
[![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)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_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).
_A no nonsense web framework for structured, dynamic and maintainable applications_
## Project Overview
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
[Odoo](https://www.odoo.com/) for its products. Owl is a modern
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
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
simple and consistent way. Owl's main features are:
- a declarative component system,
- a fine grained reactivity system similar to Vue,
- hooks
- fragments
- asynchronous rendering
- a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router
Owl components are defined with ES6 classes and xml templates, uses an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
asynchronous.
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
Quick links:
- [documentation](#documentation),
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
- [playground](https://odoo.github.io/owl/playground)
**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.
## Example
Here is a short example to illustrate interactive components:
```javascript
const { Component, useState, mount, xml } = owl;
import { Component, QWeb, useState } from "owl";
import { xml } from "owl/tags";
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value = state.value + props.increment">
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
class Root extends Component {
class App extends Component {
static template = xml`
<span>Hello Owl</span>
<Counter increment="2"/>`;
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter };
}
mount(Root, document.body);
const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
Also, all examples here uses the [`xml` helper](doc/reference/templates.md#inline-templates) to define inline templates.
Note that the counter component is made reactive with the [`useState`](doc/hooks.md#usestate)
hook. Also, all examples here uses the `xml` helper to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
[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).
If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md).
## Documentation
### Learning Owl
The complete documentation can be found [here](doc/readme.md). The most important sections are:
Are you new to Owl? This is the place to start!
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
- [Hooks](doc/hooks.md)
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
- [How to start an Owl project](doc/learning/quick_start.md)
- [How to test Components](doc/learning/how_to_test.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
### Reference
- [Overview](doc/readme.md)
- [App](doc/reference/app.md)
- [Component](doc/reference/component.md)
- [Component Lifecycle](doc/reference/component.md#lifecycle)
- [Concurrency Model](doc/reference/concurrency_model.md)
- [Dev mode](doc/reference/app.md#dev-mode)
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
- [Environment](doc/reference/environment.md)
- [Error Handling](doc/reference/error_handling.md)
- [Event Handling](doc/reference/event_handling.md)
- [Form Input Bindings](doc/reference/input_bindings.md)
- [Fragments](doc/reference/templates.md#fragments)
- [Hooks](doc/reference/hooks.md)
- [Loading Templates](doc/reference/app.md#loading-templates)
- [Mounting a component](doc/reference/app.md#mount-helper)
- [Portal](doc/reference/portal.md)
- [Precompiling templates](doc/reference/precompiling_templates.md)
- [Props](doc/reference/props.md)
- [Props Validation](doc/reference/props.md#props-validation)
- [Reactivity](doc/reference/reactivity.md)
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
- [Refs](doc/reference/refs.md)
- [Slots](doc/reference/slots.md)
- [Sub components](doc/reference/component.md#sub-components)
- [Sub templates](doc/reference/templates.md#sub-templates)
- [Templates (Qweb)](doc/reference/templates.md)
- [Translations](doc/reference/translations.md)
- [Utils](doc/reference/utils.md)
### Other Topics
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
## Installing Owl
Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
```
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl](https://github.com/odoo/owl/releases/latest)
- [owl-0.22.0.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.js)
- [owl-0.22.0.min.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all (owl) tests |
## Quick Overview
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**Environment:** the root component is special: it is created with an environment,
which should contain a `QWeb` instance. The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**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++;
}
}
```
**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 section on [event handling](doc/component.md#event-handling)
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.
## License
OWL is [LGPL licensed](./LICENSE).
+96
View File
@@ -0,0 +1,96 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<a class="btn flash" t-on-click="doSomething">Click</a>
```
with the following CSS:
```css
btn {
background-color: gray;
}
.flash {
transition: background 0.5s;
}
.flash:active {
background-color: #41454a;
transition: background 0s;
}
```
will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button.
## CSS Transitions
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us. It works on html elements and
on components, by adding and removing some css classes.
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM,
- 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
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
The `t-transition` directive can be applied on a node element or on a component.
Notes:
- more information on animations are available [here](animations.md).
- 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)
@@ -3,8 +3,8 @@
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.
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
In this page, we try to highlight some of these differences. Obviously, some
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.
## Content
@@ -14,8 +14,8 @@ discussed, feel free to open an issue/submit a PR to correct this text.
- [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering)
- [Reactivity](#reactivity)
- [Hooks](#hooks)
- [Reactiveness](#reactiveness)
- [State Management](#state-management)
## Size
@@ -24,16 +24,11 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
| Framework | Size (minified, gzipped) |
| ------------------------ | ------------------------ |
| OWL | 18kb |
| OWL | 16kb |
| Vue + VueX | 30kb |
| Vue + VueX + Vue Router | 39kb |
| React + ReactDOM + Redux | 40kb |
| jQuery | 30kb |
Note that those comparisons are not entirely fair, because we do not compare
the same exact set of features. For example, VueX and Vue Router support more
advanced use cases.
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
@@ -45,17 +40,6 @@ contrast, Owl has only one mechanism: class-based components. We believe that Ow
components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
It certainly looks like the world of UI frameworks is moving toward composition,
for many very good reasons. Owl is still good at composition (for example,
Owl supports slots, which is the primary mechanism to make generic reusable
components). But it can also use inheritance (and this is very important since
templates can also be inherited with `xpaths` transformations).
## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons,
@@ -66,23 +50,13 @@ be used by simply adding a script tag to a page.
<script src="owl.min.js" />
```
In comparison, React encourages using JSX, which necessitate a build step, and
most Vue applications uses single file components, which also necessitate a build step.
In comparison, React encourages using JSX,
which necessitate a build step, and most Vue applications uses single file
components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it
also brings a lot of benefits. And React/Vue have both a large ecosystem.
Note that since Owl is not dependant on any external tool nor libraries, it is
very easy to integrate into any build toolchain. Also, since we cannot rely on
additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](../reference/templates.md#inline-templates) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
## Templating
OWL uses its own QWeb engine, which compiles templates on the
@@ -105,8 +79,7 @@ into javascript functions. Note that Vue has a separate build which includes the
template compiler.
In contrast, most React applications do not use a templating language, but write
some JSX code, which is precompiled into plain JavaScript by a build step. This
example is done with the (kind of outdated) React class system:
some JSX code, which is precompiled into plain JavaScript by a build step.
```jsx
class Clock extends React.Component {
@@ -125,20 +98,6 @@ This has the advantage of having the full power of Javascript, but is less
structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](../reference/templates.md#inline-templates) tag helper:
```js
class Clock extends Component {
static template = xml`
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
`;
}
```
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
@@ -168,14 +127,12 @@ This may be dangerous (to stop the rendering waiting for the network), but it is
extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
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
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
more convoluted.
## Reactivity
## Reactiveness
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.
This is simple, efficient, and a little bit awkward to write.
Vue is a little bit different: it replace magically the properties in the state
@@ -188,68 +145,92 @@ 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
rendering is scheduled in the next microtask tick (promise queue).
## Hooks
## State Management
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
the React world. They solve a lot of seemingly unconnected problems: attach
reusable behavior to a component, in a composable way, extract stateful logic
from a component or reuse stateful logic between component, without changing your
component hierarchy.
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.
Here is an example of the React `useState` hook:
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.
```js
import React, { useState } from "react";
**Redux**
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
In Redux, the state is mutated by reducers. Reducers are functions
that modify the state by returning a different object:
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
```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
}
}
};
}
```
Because of the way React designed the hooks API, they only work for functional
components. But in that case, they really are powerful. Every major React library
is in the process of redesigning their API with hooks (for example,
[Redux](https://react-redux.js.org/next/api/hooks)).
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.
Vue 2 does not have hooks, but the Vue project is working on its next version,
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
This work is based on the new ideas introduced by React hooks.
**VueX**
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](../reference/hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
VueX is based on a different principle: the state is mutated through
some special functions (the mutations), which modify the state in place:
Here is what the `Counter` example above look like in Owl:
```javascript
function ({state}, payload) {
const { id, content } = payload;
const message = {id, content, completed: false};
state.messages.push(message)
}
```
```js
import { Component, Owl } from "owl";
import { xml } from "owl/tags";
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.
class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
**Owl**
count = useState({ value: 0 });
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, by inheriting the `ConnectedComponent` class.
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.state.value++;
this.env.store.dispatch("increment");
}
}
```
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
`Context` abstraction.
const counter = new Counter({ store, qweb });
```
+1226
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
# 🦉 Event Bus 🦉
It is sometimes useful to use a `Bus` to communicate informations between various
parts of the code. Owl has a very simple bus class, which manages subscriptions,
triggering events, and callbacks.
```js
const bus = new owl.EventBus();
bus.on("some-event", null, function(...args) {
console.log(...args);
});
bus.trigger("some-event", 1, 2, 3);
// [1,2,3] will be logged to the console
```
Its API is:
| Method | Description |
| -------------------------------- | --------------------------------- |
| `on(eventType, owner, callback)` | add a listener |
| `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions |
Note that the [`Store`](store.md) is an example of an `EventBus`.
+275
View File
@@ -0,0 +1,275 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [Example: Mouse Position](#example-mouse-position)
- [Example: Autofocus](#example-autofocus)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
- [`useRef`](#useref)
- [`useSubEnv`](#useSubEnv)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Example: autofocus
Hooks can be combined to create the desired effect. For example, the following
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
to create an easy way to focus an input whenever it appears in the DOM:
```js
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
constructor(...args) {
super(...args);
useAutofocus("myinput");
}
}
```
## Reference
### One rule
There is only one rule: every hook for a component have to be called in the
constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
this is what enables component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
class Counter extends owl.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++;
}
}
```
### `onMounted`
`onMounted` is not an user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not an user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references needs to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to *scope* that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub component, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it let a component add some
information to the environment in a way that only the component and its children
can access it:
```js
class FormComponent extends Component {
constructor(...args) {
super(...args);
const model = makeModel();
useSubEnv({ model });
}
}
```
The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
-95
View File
@@ -1,95 +0,0 @@
# 🦉 How to test Components 🦉
## Content
- [Overview](#overview)
- [Unit Tests](#unit-tests)
## Overview
It is a good practice to test applications and components to ensure that they
behave as expected. There are many ways to test a user interface: manual
testing, integration testing, unit testing, ...
In this section, we will discuss how to write unit tests for components.
## Unit Tests
Writing unit tests for Owl components really depends on the testing framework
used in a project. But usually, it involves the following steps:
- create a test file: for example `SomeComponent.test.js`,
- in that file, import the code for `SomeComponent`,
- add a test case:
- create a real DOM element to use as test fixture,
- create a test environment
- create an instance of `SomeComponent`, mount it to the fixture
- interact with the component and assert some properties.
To help with this, it is useful to have a `helper.js` file that contains some
common utility functions:
```js
let lastFixture = null;
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture;
}
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
```
With such a file, a typical test suite for Jest will look like this:
```js
// in SomeComponent.test.js
import { SomeComponent } from "../../src/ui/SomeComponent";
import { nextTick, makeTestFixture } from '../helpers';
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = await mount(SomeComponent, fixture, { props });
// do some assertions
expect(...).toBe(...);
fixture.querySelector('button').click();
await nextTick();
// some other assertions
expect(...).toBe(...);
});
});
```
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
make sure that the DOM is up-to-date.
-399
View File
@@ -1,399 +0,0 @@
# 🦉 How to start an Owl project 🦉
## Content
- [Overview](#overview)
- [Simple html file](#simple-html-file)
- [With a static server](#with-a-static-server)
- [Standard Javascript project](#standard-javascript-project)
## Overview
Each software project has its specific needs. Many of these needs can be solved
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
Because of that, it is usually not simple to just start a project. Some
frameworks provide their own tooling to help with that. But then, you have to
integrate and learn how these applications work.
Owl is designed to be used with no tooling at all. Because of that, Owl can
"easily" be integrated in a modern build toolchain. In this section, we will
discuss a few different setups to start a project. Each of these setups has
advantages and disadvantages in different situations.
## Simple html file
The simplest possible setup is the following: a simple javascript file with your
code. To do that, let us create the following file structure:
```
hello_owl/
index.html
owl.js
app.js
```
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
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:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
```
And `app.js` should look like this:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
mount(Root, document.body);
```
Now, simply loading this html file in a browser should display a welcome message.
This setup is not fancy, but it is extremely simple. There are no tooling at
all required. It can be slightly optimized by using the minified build of Owl.
## With a static server
The previous setup has a big disadvantage: the application code is located in a
single file. Obviously, we could split it in several files and add multiple
`<script>` tags in the html page, but then we need to make sure the script are
inserted in the proper order, we need to export each file content in global
variables and we lose autocompletion across files.
There is a low tech solution to this issue: using native javascript modules.
This however has a requirement: for security reasons, browsers will not accept
modules on content served through the `file` protocol. This means that we need
to use a static server.
Let us start a new project with the following file structure:
```
hello_owl/
src/
index.html
main.js
owl.js
root.js
```
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).
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:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
```
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.
Here is the content of `root.js` and `main.js`:
```js
// root.js ----------------------------------------------------------------------
const { Component, mount, xml } = owl;
export class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js ---------------------------------------------------------------------
import { Root } from "./root.js";
mount(Root, document.body);
```
The `main.js` file imports the `root.js` file. Note that the import statement has
a `.js` suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion.
Now, to execute this code, we need to serve the `src` folder statically. A low
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
```
$ cd src
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
```
Another more "javascripty" way to do it is to create a `npm` application. To do
that, we can add the following `package.json` file at the root of the project:
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
```
We can now install the `serve` tool with the command `npm install`, and then,
start a static server with the simple `npm run serve` command.
## Standard Javascript project
The previous setup works, and is certainly good for some usecases, including
quick prototyping. However, it lacks some useful features, such as livereload,
a test suite, or bundling the code in a single file.
Each of these features, and many others, can be done in many different ways.
Since it is really not trivial to configure such a project, we provide here an
example that can be used as a starting point.
Our standard Owl project has the following file structure:
```
hello_owl/
public/
index.html
src/
components/
Root.js
main.js
tests/
components/
Root.test.js
helpers.js
.gitignore
package.json
webpack.config.js
```
This project as a `public` folder, meant to contain all static assets, such as
images and styles. The `src` folder has the javascript source code, and finally,
`tests` contains the test suite.
Here is the content of `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
```
Note that there are no `<script>` tag here. They will be injected by webpack.
Now, let's have a look at the javascript files:
```js
// src/components/Root.js -------------------------------------------------------
import { Component, xml, useState } from "@odoo/owl";
export class Root extends Component {
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
// src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl";
import { Root } from "./components/Root";
mount(Root, document.body);
// tests/components/Root.test.js ------------------------------------------------
import { Root } from "../../src/components/Root";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("Root", () => {
test("Works as expected...", async () => {
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
// tests/helpers.js ------------------------------------------------------------
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
```
Finally, here is the configuration files `.gitignore`, `package.json` and
`webpack.config.js`:
```
node_modules/
package-lock.json
dist/
```
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
```
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
};
```
With this setup, we can now use the following script commands:
```
npm run build # build the full application in prod mode in dist/
npm run dev # start a dev server with livereload
npm run test # run the jest test suite
```
-993
View File
@@ -1,993 +0,0 @@
# 🦉 OWL Tutorial: TodoApp 🦉
For this tutorial, we will build a very simple Todo list application. The app
should satisfy the following requirements:
- let the user create and remove tasks
- tasks can be marked as completed
- tasks can be filtered to display active/completed tasks
This project will be an opportunity to discover and learn some important Owl
concepts, such as components, store, and how to organize an application.
## Content
1. [Setting up the project](#1-setting-up-the-project)
2. [Adding a first component](#2-adding-a-first-component)
3. [Displaying a list of tasks](#3-displaying-a-list-of-tasks)
4. [Layout: some basic css](#4-layout-some-basic-css)
5. [Extracting Task as a subcomponent](#5-extracting-task-as-a-subcomponent)
6. [Adding tasks (part 1)](#6-adding-tasks-part-1)
7. [Adding tasks (part 2)](#7-adding-tasks-part-2)
8. [Toggling tasks](#8-toggling-tasks)
9. [Deleting tasks](#9-deleting-tasks)
10. [Using a store](#10-using-a-store)
11. [Saving tasks in local storage](#11-saving-tasks-in-local-storage)
12. [Filtering tasks](#12-filtering-tasks)
13. [The Final Touch](#13-the-final-touch)
14. [Final Code](#final-code)
## 1. Setting up the project
For this tutorial, we will do a very simple project, with static files and
no additional tooling. The first step is to create the following file structure:
```
todoapp/
index.html
app.css
app.js
owl.js
```
The entry point for this application is the file `index.html`, which should have
the following content:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</body>
</html>
```
Then, `app.css` can be left empty for now. It will be useful later on to style
our application. `app.js` is where we will write all our code. For now, let's
just put the following code:
```js
(function () {
console.log("hello owl", owl.__info__.version);
})();
```
Note that we put everything inside an immediately executed function to avoid leaking
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
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
built to be bundled by other tools).
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
message such as `hello owl 2.x.y` in the console.
## 2. Adding a first component
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
content of the function in `app.js` by the following code:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>todo app</div>`;
}
mount(Root, document.body);
```
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
mount it in the document body.
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.
However, this is a very small project, and we want to keep it as simple as possible.
Note 2: this tutorial uses the static class field syntax. This is not yet
supported by all browsers. Most real projects will transpile their code, so this
is not a problem, but for this tutorial, if you need the code to work on every
browser, you will need to translate each `static` keyword to an assignation to
the class:
```js
class App extends Component {}
App.template = xml`<div>todo app</div>`;
```
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
is nice, but there is no syntax highlighting, and this makes it very easy to
have malformed xml. Some editors support syntax highlighting for this situation.
For example, VS Code has an addon `Comment tagged template`, which, if installed,
will properly display tagged templates:
```js
static template = xml /* xml */`<div>todo app</div>`;
```
Note 4: Large applications will probably want to be able to translate templates.
Using inline templates makes it slightly harder, since we need additional tooling
to extract the xml from the code, and to replace it with the translated values.
## 3. Displaying a list of tasks
Now that the basics are done, it is time to start thinking about tasks. To
accomplish what we need, we will keep track of the tasks as an array of objects
with the following keys:
- `id`: a number. It is extremely useful to have a way to uniquely identify
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
each task.
- `text`: a string, to explain what the task is about.
- `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
data and a template to the `App` component:
```js
class Root extends Component {
static template = xml/* xml */ `
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.text"/></span>
</div>
</t>
</div>`;
tasks = [
{
id: 1,
text: "buy milk",
isCompleted: true,
},
{
id: 2,
text: "clean house",
isCompleted: false,
},
];
}
```
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
through the tasks. It can find the `tasks` list from the component, since the
component is the rendering context. Note that we use the `id` of each task as a
`t-key`, which is very common. There are two css classes: `task-list` and `task`,
that we will use in the next section.
Finally, notice the use of the `t-att-checked` attribute:
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
it dynamic. Owl will evaluate the expression and set it as the value of the
attribute.
## 4. Layout: some basic css
So far, our task list looks quite bad. Let us add the following to `app.css`:
```css
.task-list {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.task {
font-size: 18px;
color: #111111;
}
```
This is better. Now, let us add an extra feature: completed tasks should be
styled a little differently, to make it clearer that they are not as important.
To do that, we will add a dynamic css class on each task:
```xml
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
```
```css
.task.done {
opacity: 0.7;
}
```
Notice that we have here another use of a dynamic attribute.
## 5. Extracting Task as a subcomponent
It is now clear that there should be a `Task` component to encapsulate the look
and behavior of a task.
This `Task` component will display a task, but it cannot _own_ the state of the
task: a piece of data should only have one owner. Doing otherwise is asking for
trouble. So, the `Task` component will get its data as a `prop`. This means that
the data is still owned by the `App` component, but can be used by the `Task`
component (without modifying it).
Since we are moving code around, it is a good opportunity to refactor the code
a little bit:
```js
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.text"/></span>
</div>`;
static props = ["task"];
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml /* xml */`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>`;
static components = { Task };
tasks = [
...
];
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
mount(Root, document.body, {dev: true});
```
A lot of stuff happened here:
- first, we have now a sub component `Task`, defined on top of the file,
- whenever we define a sub component, it needs to be added to the static
[`components`](../reference/component.md#static-properties)
key of its parent, so Owl can get a reference to it,
- 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
`task`. If this is not the case, Owl will throw an
[error](../reference/props.md#props-validation). This is extremely
useful when refactoring components
- finally, to activate the props validation, we need to set Owl's
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
of the `mount` 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
checks and validations.
## 6. Adding tasks (part 1)
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.
But this input will be outside of the task list, so we need to adapt `Root`
template, js, and css:
```xml
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
```
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', text);
// todo
}
}
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
```
We now have a working input, which log to the console whenever the user adds a
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
focusing the input.
We need to execute code when the `Root` component is ready (mounted). Let's do
that using the `onMounted` hook. 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
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
```
```js
// on top of file:
const { Component, mount, xml, useRef, onMounted } = owl;
```
```js
// in App
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
}
```
This is a very common situation: whenever we need to perform some actions depending
on the lifecycle of a component, we need to do it in the `setup` method, by using
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
then in the `onMounted` hook, we simply focus the html element.
## 7. Adding tasks (part 2)
In the previous section, we did everything except implement the code that actually
create tasks! So, let us do that now.
We need a way to generate unique `id` numbers. To do that, we will simply add a
`nextId` number in `App`. At the same time, let us remove the demo tasks in `App`:
```js
nextId = 1;
tasks = [];
```
Now, the `addTask` method can be implemented:
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
if (text) {
const newTask = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
```
This almost works, but if you test it, you will notice that no new task is ever
displayed when the user press `Enter`. But if you add a `debugger` or a
`console.log` statement, you will see that the code is actually running as
expected. The problem is that Owl has no way of knowing that it needs to rerender
the user interface. We can fix the issue by making `tasks` reactive, with the
[`useState`](../reference/hooks.md#usestate) hook:
```js
// on top of the file
const { Component, mount, xml, useRef, onMounted, useState } = owl;
// replace the task definition in App with the following:
tasks = useState([]);
```
It now works as expected!
## 8. Toggling tasks
If you tried to mark a task as completed, you may have noticed that the text
did not change in opacity. This is because there is no code to modify the
`isCompleted` flag.
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.
However, for now, that's what we will do (this will be improved in a later step).
In `Task`, change the `input` to:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
```
and add the `toggleTask` method:
```js
toggleTask() {
this.props.task.isCompleted = !this.props.task.isCompleted;
}
```
## 9. Deleting tasks
Let us now add the possibility do delete tasks. This is different from the previous
feature: deleting task has to be done on the task itself, but the actual operation
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:
```xml
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span>
</div>
```
```css
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
```
```js
static props = ["task", "onDelete"];
deleteTask() {
this.props.onDelete(this.props.task);
}
```
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
component:
```xml
<Task task="task" onDelete.bind="deleteTask"/>
```
```js
deleteTask(task) {
const index = this.tasks.findIndex(t => t.id === task.id);
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
Looking at the code, it is apparent that all the code handling tasks is scattered
all around the application. Also, it mixes UI code and business logic
code. Owl does not provide any high level abstraction to manage business logic,
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
Let us use it in our application to implement a central store. This is a pretty
large refactoring (for our application), since it involves extracting all task
related code out of the components. Here is the new content of the `app.js` file:
```js
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
nextId = 1;
tasks = [];
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
return reactive(new TaskList());
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<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)"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* 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="store.tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = {
store: createTaskStore(),
};
mount(Root, document.body, { dev: true, env });
```
## 11. Saving tasks in local storage
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.
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
listen to any change.
```js
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
// ...
}
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;
}
```
The key point is that the `reactive` function takes a callback that will be called
every time an observed value is changed. Note that we need to call the `saveTasks`
method initially to make sure we observe all current values.
## 12. Filtering tasks
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
need to keep track of the state of the filter in `Root`, then filter the visible
tasks according to its value.
```js
class Root extends Component {
static template = xml /* 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">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</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
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
Notice here that we set dynamically the css class of the filter with the object
syntax.
## 13. The Final Touch
Our list is feature complete. We can still add a few extra details to improve
the user experience.
1. Add a visual feedback when the user mouse is over a task:
```css
.task:hover {
background-color: #def0ff;
}
```
2. Make the text of a task clickable, to toggle its checkbox:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
```
3. Strike the text of completed task:
```css
.task.done label {
text-decoration: line-through;
}
```
## Final code
Our application is now complete. It works, the UI code is well separated from
the business logic code, it is testable, all under 150 lines of code (template
included!).
For reference, here is the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</body>
</html>
```
```js
(function () {
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// 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 = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
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
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<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"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* 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">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
this.filter = useState({ value: "all" });
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
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;
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(Root, document.body, { dev: true, env });
})();
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task:hover {
background-color: #def0ff;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
.task.done {
opacity: 0.7;
}
.task.done label {
text-decoration: line-through;
}
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
-68
View File
@@ -1,68 +0,0 @@
# 🦉 Notes On Owl Architecture 🦉
We explain here how Owl is designed
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
Roughly speaking, Owl has 5 main parts:
- a virtual dom system (in `src/blockdom`)
- a component system (in `src/component`)
- a template compiler (located in the `src/compiler` folder)
- a small runtime code to tie them together (in `src/app`)
- a reactivity system (in `src/reactivity.ts`)
There are some other files, but the core of Owl can be understood with these
five main parts.
The virtual dom is an optimized virtual dom based on blocks, which supports
multi blocks (for fragments). Everything that owl renders is internally
represented by a virtual node. The job of the virtual dom is to efficiently
represent the current state of the application, and to build an actual DOM
representation when needed, or update the DOM whenever it is needed.
- some other helpers/smaller scale stuff
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
-98
View File
@@ -1,98 +0,0 @@
# 🦉 Notes On Owl Compiled Templates 🦉
This page will explain what an Owl compiled template look like. This is a
technical document intended for developers interested in understanding how Owl
works internally.
Broadly speaking, Owl compiles templates into a javascript function (a closure)
that returns a function (the "render" function). The point of the closure is to
have a place to store all values specific to the template (in particular, "blocks").
Once a template is compiled, its closure function is called once to get the
render function, and from then on, only the render function is used.
The render function takes some context (and some additional information) and
return a virtual dom representation of the rendered template, as a block tree.
A block tree is a very light weight representation that only contains the dynamic
part of the template, and its structure. It is actually independant of the
static part of the templates (which are contained in the blocks captured by the
closure). This means that the work performed at render time is only to collect
dynamic data, and to describe the block structure of the result.
It looks like this, in pseudo code:
```js
function closure(bdom, helpers) {
// here is some place to put stuff specific to the template, such as
// blocks
...
return function render(context, node, key) {
// only build here all dynamic parts of the template
// build a block tree
return tree;
}
}
```
Now, let us see an example. Consider the following template:
```xml
<div class="some-class">
<div class="blabla">
<span><t t-esc="state.value"/></span>
</div>
<t t-if="state.info">
<p class="info" t-att-class="someAttribute">
<t t-esc="state.info"/>
</p>
</t>
<SomeComponent value="value"/>
</div>
```
If you look carefully, there are 5 dynamic things:
- a text value (the first `t-esc`),
- a sub block (the `t-if`),
- a dynamic attribute (the `t-att-class` attribute),
- another text value (the second `t-esc`),
- and finally, a sub component
Here is the compiled code for this template:
```js
function closure(bdom, helpers) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
);
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
return function render(ctx, node, key = "") {
let b2, b3;
let txt1 = ctx["state"].value;
if (ctx["state"].info) {
let attr1 = ctx["someAttribute"];
let txt2 = ctx["state"].info;
b2 = block2([attr1, txt2]);
}
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
return block1([txt1], [b2, b3]);
};
}
```
The values captured in the closure capture the static part of the template: we
define here two blocks (which contains a template node, that can be deep cloned
whenever a block is mounted). Then the render function only describes the block
tree structure of the result, depending on the context. This means that we
minimize the amount of work done at render time.
Then, when we want to patch the dom, Owl will uses the `patch` function from
blockdom, which then will diff the block tree, and deep clone new blocks whenever
a new block is inserted, keep track of dynamic parts of each block, and update
them accordingly.
With this design, the cost of rendering a template is proportional to the number
of dynamic values, and not to the size of the template.
-180
View File
@@ -1,180 +0,0 @@
# 🦉 Why Owl ? 🦉
The common wisdom is that one should not reinvent the wheel, because that would
waste effort and resources. It is certainly true in many cases. A javascript
framework is a considerable investment, so it is quite logical to ask the question:
why did Odoo decide to make OWL instead of using a standard/well known framework,
such as React or Vue?
As you might expect, the answer to that question is not simple. But most of the
reasons discussed in this page are a consequence from a single fact: Odoo is
extremely modular.
This means, for example, that the core parts of Odoo are not aware, before runtime,
of what files will be loaded/executed, or what will be the state of the UI. Because
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
really an application with a user interface. It is an application which generates
a dynamic user interface. And most frameworks are not up to the task.
Betting on Owl was not an easy choice to make, because there certainly are a lot
of conflicting needs that we want to carefully balance. Choosing anything other
than a well known framework is bound to be controversial. This page will explain
some of the reason why we still believe that building Owl is a worthwile
endeavour.
## Strategy
It is true that we want to keep control of our technology, in the sense that we
do not want to depend on Facebook or Google, or any other large (or small)
company. If they decide to change their license, or to go in a direction that
will not work for us, this may be a problem. This is even more true because
Odoo is not a conventional javascript application, and our needs are probably
quite different as most other applications.
## Class components
It is clear that the biggest frameworks are moving away from class components.
There is an implicit assumption that class components are terrible, and that
functional programming is the way to go. React even goes as far as to say that
classes are confusing for developers.
While there is some truth to that, and to the fact that composition is certainly
a good mechanism for code reuse, we believe that classes and inheritance are
important tools.
Sharing code between generic components with inheritance is the way Odoo built
its web client. And it is clear that inheritance is not the root of all evils.
It is often a perfectly simple and appropriate solution. What matter most is
the architectural decisions.
Also, Odoo has another specific use out of class components: each method of a
class provides an extension point for addons. This may not be a clean architecture
pattern, but it is a pragmatic decision that served Odoo well: classes are
sometimes monkey-patched to add behaviour from the outside. A little bit like
mixins, but from the outside.
Using React or Vue would make it significantly harder to monkey patch components,
because a lot of the state is hidden in their internals.
## Tooling
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:
since the assets are totally dynamic (and could change whenever the user installs
or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal.
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
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
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!)
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
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
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
can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag.
## Template based
Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity.
Because of that, we still expect to write our templates in an XML document.
Weirdly enough, no major framework uses XML to store templates, even though it
is extremely convenient.
So, using React or Vue means that we need to make a template compiler. For React,
that would be a compiler that would take a QWeb template, and convert it to a
React render function. For Vue, it would convert it to a Vue template. Then
we need to bundle the vue template compiler as well.
Not only this would be complex (compiling a templating language into another is
not an easy task), but it would negatively impact the developer experience as
well. Writing Vue or React components in a QWeb template would certainly be
awkward, and very confusing.
## Developer Experience
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
easy as possible.
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
overwhelmed with the frontend world: functional components, hooks, and many other
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
somehow join various namespaces into one, under the hood, and add various internal
keys. Svelte transform the code. React require that state transformations are
deep, and not shallow.
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
reactivity system is explicit, not implicit. The scoping rules are obvious. In
case of doubt, we err on the side of not implementing a feature.
It is certainly different from React or Vue, but at the same time, kind of
familiar for experienced developers.
## JIT compilation
There is also a clear trend in the frontend world to compile code
as much as possible ahead of time. Most frameworks will compile templates ahead
of time. And now Svelte is trying to compile the JS code away, so it can remove
itself from the bundle.
This is certainly reasonable for many usecases. However, this is not what Odoo
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.
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
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
immediately.
## Reactivity
There are other design choices that we feel are not optimal in other frameworks.
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
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 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
(basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory.
## Concurrency
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
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
only if it has not been cancelled by subsequent user actions).
React has now an experimental concurrent mode, but it was not ready when Owl
started. Vue has not really an equivalent API (suspense is not what we need).
Also, React concurrent mode is complex to use. Concurrency was one of the rare
strong point of the former Odoo js framework (widgets), and we feel that Owl has
now a very strong concurrent mode, which is simple and powerful at the same time.
## Conclusion
This lengthy discussion showed that there are many small and not so small reasons
that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs.
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.
And that is why we built Owl 🦉.
+20
View File
@@ -0,0 +1,20 @@
# 🦉 Observer 🦉
Owl need to be able to react to state changes. For example, whenever the state
of a component is changed, we need to rerender it. To help with that, we have
an Observer class. Its job is to observe some object state, and react to any
change. To do that, it recursively replace all keys of the observed state by
getters and setters.
For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2;
```
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
+103
View File
@@ -0,0 +1,103 @@
# 🦉 Quick Start 🦉
## Static Server
Let us assume that we have a static server running somewhere. We could then
simply add an html page with a few extra files.
### HTML and CSS
In a file `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My OWL App</title>
<link href="app.css" rel="stylesheet" />
<script src="owl-X.Y.Z.js"></script>
</head>
<body>
<div id="main"></div>
<script src="app.js" type="module"></script>
</body>
</html>
```
In `app.css`:
```css
button {
color: darkred;
font-size: 30px;
width: 220px;
}
```
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
### XML
In `templates.xml`:
```xml
<templates>
<button t-name="clickcounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
### JS
To build an application (or a sub-part of an application), we need two things:
- an environment: it is the global context in which we are working. It needs to
contain a QWeb instance (preloaded with templates), and anything else that we
need. In practice, it could context some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode).
- a description of the user interface: there should be a root component, which can
have sub components
Here are a few steps that we may take to get started:
- get the templates
- create a qweb engine, with the templates
- create an environment
- create an instance of the root component
- mount the root component to a DOM element
Let us now add the javascript to make it work, in `app.js`:
```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const counter = new ClickCounter(env);
const target = document.getElementById("main");
await counter.mount(target);
}
start();
```
+529
View File
@@ -0,0 +1,529 @@
# 🦉 QWeb 🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Debugging](#debugging)
## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message">
</li>
</ul>
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
## Directives
We present here a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
It's API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial
templates (see `addTemplates` for more information on format of the string).
```js
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register an global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsability: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White Spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root Nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that applies on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context (typically, the component):
```xml
<p t-if="user.birthday === today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves:
```xml
<div>hello</div> <!–– rendered as itself ––>
```
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
```xml
<p><t t-esc="value"/></p>
```
rendered with the value `value` set to `42` in the rendering context yields:
```html
<p>42</p>
```
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe.
```xml
<p><t t-raw="value"/></p>
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
```html
<p><span>foo</span></p>
```
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
1. a `t-value` attribute containing an expression, and the result of its
evaluation will be set:
```xml
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
```
will print `3`. Note that the evaluation is done at rendering time, not at
compilte time.
2. if there is no `t-value` attribute, the nodes body is saved and its value is
set as the variables value:
```xml
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
```
will generate `&lt;li&gt;ok&lt;/li&gt;` (the content is escaped as we used the `t-esc` directive)
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
```xml
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
```
The element is rendered if the condition (evaluated with the current rendering
context) is true:
```xml
<div>
<p>ok</p>
</div>
```
but if the condition is false it is removed from the result:
```xml
<div>
</div>
```
The conditional rendering applies to the bearer of the directive, which does not
have to be `<t>`:
```xml
<div>
<p t-if="condition">ok</p>
</div>
```
will give the same results as the previous example.
Extra conditional branching directives `t-elif` and `t-else` are also available:
```xml
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
```
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
```xml
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation:
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
collection to iterate on, and a second parameter `t-as` providing the name to use
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-esc="i"/></p>
</t>
```
will be rendered as:
```xml
<p>1</p>
<p>2</p>
<p>3</p>
```
Like conditions, `t-foreach` applies to the element bearing the directives attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
`t-foreach` can iterate on an array (the current item will be the current value)
or an object (the current item will be the current key).
In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced by the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratees size be
available
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="False"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i">
<t t-set="existing_variable" t-value="True"/>
<t t-set="new_variable" t-value="True"/>
<!-- existing_variable and new_variable now True -->
</p>
<!-- existing_variable always True -->
<!-- new_variable undefined -->
```
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
templates), using the `t-call` directive:
```xml
<div t-name="other-template">
<p><t t-value="var"/></p>
</div>
<div t-name="main-template">
<t t-set="var" t-value="owl"/>
<t t-call="other-template"/>
</div>
```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape.
Sometimes, one might want to pass information to the sub template. In that case,
the content of the body of the `t-call` directive is available as a special
magic variable `0`:
```xml
<t t-name="other-template">
This template was called with content:
<t t-raw="0"/>
</t>
<div t-name="main-template">
<t t-call="other-template">
<em>content</em>
</t>
</div>
```
will result in :
```xml
<div>
This template was called with content:
<em>content</em>
</div>
```
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
`t-debug` adds a debugger statement during template rendering:
```xml
<t t-if="a_test">
<t t-debug="">
</t>
```
will stop execution if the browser dev tools are open.
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
```xml
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
```
will print 42 to the console
+55 -40
View File
@@ -1,50 +1,65 @@
# 🦉 Owl overview 🦉
# 🦉 OWL Documentation 🦉
Here is a list of everything exported by the Owl library:
## Owl Content
Main entities:
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
- [`Component`](reference/component.md): the main class to define a concrete Owl component
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
```
owl
Component
QWeb
useState
core
EventBus
Observer
hooks
onMounted
onWillUnmount
onWillPatch
onPatched
useState
useRef
useSubEnv
router
Link
RouteComponent
Router
store
Store
ConnectedComponent
tags
xml
utils
debounce
escape
loadJS
loadTemplates
whenReady
```
Reactivity
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
## Reference
Lifecycle hooks:
- [Animations](animations.md)
- [Component](component.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)
- [Store](store.md)
- [Tags](tags.md)
- [Utils](utils.md)
- [Virtual DOM](vdom.md)
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
## Learning Resources
Other hooks:
- [Quick Start](quick_start.md)
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
- [`useRoots`](reference/hooks.md#useroots): returns an object that provides access to all root nodes or htmlelements
## Miscellaneous
Utility/helpers:
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
-123
View File
@@ -1,123 +0,0 @@
# 🦉 App 🦉
## Content
- [Overview](#overview)
- [API](#api)
- [Configuration](#configuration)
- [`mount` helper](#mount-helper)
- [Loading templates](#loading-templates)
## Overview
Every Owl application has a root element, a set of templates, an environment and
possibly a few other settings. The `App` class is a simple class that represents
all of these elements. Here is an example:
```js
const {Component, App } = owl;
class MyComponent extends Component { ... }
const app = new App(MyComponent, { props: {...}, templates: "..."});
app.mount(document.body);
```
The basic workflow is: create an `App` instance configured with the root
component, the templates, and possibly other settings. Then, we mount that
instance somewhere in the DOM.
## API
- **`constructor(Root[, config])`**: first argument should be a component class (not
an instance), and the optional second argument is a configuration object (see below).
- **`mount(target, options)`**: first argument is an html element, and the optional
second argument is an object with mounting options (see below). Mount the app
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
method returns a promise that resolves to the component instance whenever it
is complete.
The `option` object is an object with the following keys:
- **`position (string)`**: either `first-child` or `last-child`. This option determines
the position of the application in the target: either first or last child.
- **`destroy()`**: destroys the application
## Configuration
The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
[`dev` mode](#dev-mode);
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
that Owl will not log a message to warn that Owl is in `dev` mode.
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
templates (see [translations](translations.md))
- **`templates (string | xml document)`**: all the templates that will be used by
the components created by the application.
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
## `mount` helper
Note that there is a `mount` helper to do that in just a line:
```js
const { mount, Component } = owl;
class MyComponent extends Component {
...
}
mount(MyComponent, document.body, { props: {...}, templates: "..."});
```
Here is the `mount` function signature:
**`mount(Component, target, config)`** with the following arguments:
- **`Component`**: a component class (Root component of the app)
- **`target`**: an html element, where the component will be mounted as last child
- **`config (optional)`**: a config object (the same as the App config object)
Most of the time, the `mount` helper is more convenient, but whenever one needs
a reference to the actual Owl App, then using the `App` class directly is
possible.
## Loading templates
Most applications will need to load templates whenever they start. Here is
what it could look like in practice:
```js
// in the main js file:
const { loadFile, mount } = owl;
// async, so we can use async/await
(async function setup() {
const templates = await loadFile(`/some/endpoint/that/return/templates`);
const env = {
_t: someTranslateFn,
templates,
// possibly other stuff
};
mount(Root, document.body, { env });
})();
```
## Dev mode
Dev mode activates some additional checks and developer amenities:
- [Props validation](./props.md#props-validation) is performed
- [t-foreach](./templates.md#loops) loops check for key unicity
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
- onWillStart and onWillUpdateProps will emit a warning in the console when they
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
-455
View File
@@ -1,455 +0,0 @@
# 🦉 Owl Component 🦉
## Content
- [Overview](#overview)
- [Properties and methods](#properties-and-methods)
- [Static Properties](#static-properties)
- [Lifecycle](#lifecycle)
- [`setup`](#setup)
- [`willStart`](#willstart)
- [`willRender`](#willrender)
- [`rendered`](#rendered)
- [`mounted`](#mounted)
- [`willUpdateProps`](#willupdateprops)
- [`willPatch`](#willpatch)
- [`patched`](#patched)
- [`willUnmount`](#willunmount)
- [`willDestroy`](#willdestroy)
- [`onError`](#onerror)
- [Sub components](#sub-components)
- [Dynamic Sub components](#dynamic-sub-components)
- [`status` helper](#status-helper)
## Overview
An Owl component is a small class which represents some part of the user interface.
It is part of a component tree, and has an [environment](environment.md) (`env`),
which is propagated from a parent to its children.
OWL components are defined by subclassing the `Component` class. For example,
here is how a `Counter` component could be implemented:
```javascript
const { Component, xml, useState } = owl;
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++;
}
}
```
In this example, we use the `xml` helper to define inline templates, and the
`useState` hook, which returns a reactive version of its argument (see the page
on reactivity).
## Properties and methods
The `Component` class has a very small API.
- **`env (object)`**: the component [environment](environment.md)
- **`props (object)`**: this is an object containing all the [props](props.md) given by
the parent to a child component
Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component (otherwise you risk
unintended effects, since the parent may not be aware of the change)!!
The `props` can be modified dynamically by the parent. In that case, the
component will go through the following lifecycle methods: `willUpdateProps`,
`willPatch` and `patched`.
* **`render(deep[=false])`**: calling this method directly will cause a rerender. Note
that with the reactivity system, this should be rare to have to do it manually.
Also, the rendering operation is asynchronous, so the DOM will only be updated
slightly later (at the next animation frame, if no component delays the
rendering)
By default, the render initiated by this method will stop at each child
component if their props are (shallow) equal. To force a render to update
all child components, one can use the optional `deep` argument. Note that the
value of the `deep` argument needs to be a boolean, not a truthy value.
## Static Properties
- **`template (string)`**: this is the name of the template that
will render the component. Note that there is a helper `xml` to
make it easy to define an inline template.
* **`components (object, optional)`**: if given, this is an object that contains
the classes of any sub components needed by the template.
```js
class ParentComponent extends owl.Component {
static components = { SubComponent };
}
```
* **`props (object, optional)`**: if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](props.md#props-validation) for more information.
```js
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true,
};
}
```
- **`defaultProps (object, optional)`**: if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead. See
[default props](props.md#default-props) for more information
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
}
```
## Lifecycle
A solid and robust component system needs a complete lifecycle system to help
developers write components. Here is a complete description of the lifecycle of
a Owl component:
| Method | Hook | Description |
| --------------------------------------- | ------------------- | ---------------------------------------------------------------------- |
| **[setup](#setup)** | none | setup |
| **[willStart](#willstart)** | `onWillStart` | async, before first rendering |
| **[willRender](#willrender)** | `onWillRender` | just before component is rendered |
| **[rendered](#rendered)** | `onRendered` | just after component is rendered |
| **[mounted](#mounted)** | `onMounted` | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdateprops)** | `onWillUpdateProps` | async, before props update |
| **[willPatch](#willpatch)** | `onWillPatch` | just before the DOM is patched |
| **[patched](#patched)** | `onPatched` | just after the DOM is patched |
| **[willUnmount](#willunmount)** | `onWillUnmount` | just before removing component from DOM |
| **[willDestroy](#willdestroy)** | `onWillDestroy` | just before component is destroyed |
| **[error](#onerror)** | `onError` | catch and handle errors (see [error handling page](error_handling.md)) |
### `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 the proper place 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` is an asynchronous hook that can be implemented to
perform some (most of the time asynchronous) action before the initial rendering of a component.
It will be called exactly once before the initial rendering. It is useful
in some cases, for example, to load external assets (such as a JS library)
before the component is rendered. Another use case is to load data from a server.
The `onWillStart` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillStart(async () => {
this.data = await this.loadData()
});
}
```
At this point, the component is not yet rendered. Note that slow `willStart`
code will slow down the rendering of the user interface. Therefore, some care
should be made to make this method as fast as possible.
Note that if there are more than one `onWillStart` registered callback, then they
will all be run in parallel.
### `willRender`
It is uncommon but it may happen that one need to execute code just before a
component is rendered (more precisely, when its compiled template function is executed).
To do that, one can use the `onWillRender` hook:
```javascript
setup() {
onWillRender(() => {
// do something
});
}
```
`willRender` hooks are called just before rendering templates, parent first,
then children.
### `rendered`
It is uncommon but it may happen that one need to execute code just after a
component is rendered (more precisely, when its compiled template function is executed).
To do that, one can use the `onRendered` hook:
```javascript
setup() {
onRendered(() => {
// do something
});
}
```
`rendered` hooks are called just after rendering templates, parent first,
then children. Note that at this moment, the actual DOM may not exist yet (if
it is the first rendering), or is not updated yet. This will be dom in the next
animation frame as soon as all the components are ready.
### `mounted`
The `mounted` hook is called each time a component is attached to the
DOM, after the initial rendering. At this point, the component is considered
_active_. This is a good place to add some listeners, or to interact with the
DOM, if the component needs to perform some measure for example.
It is the opposite of `willUnmount`. If a component has been mounted, it will
always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First,
children, then parents.
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
Doing so will cause a rerender, which will not be perceptible by the user, but
will slightly slow down the component.
The `onMounted` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onMounted(() => {
// do something here
});
}
```
### `willUpdateProps`
The `willUpdateProps` is an asynchronous hook, called just before new props
are set. This is useful if the component needs to perform an asynchronous task,
depending on the props (for example, assuming that the props are
some record Id, fetching the record data).
The `onWillUpdateProps` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillUpdateProps(nextProps => {
return this.loadData({id: nextProps.id});
});
}
```
Notice that it receives the next props for the component.
This hook is not called during the first render (but `willStart` is called
and performs a similar job). Also, as most of the hooks, it is called in the
usual order: parents first, then children.
### `willPatch`
The willPatch hook is called just before the DOM patching process starts.
It is not called on the initial render. This is useful to read
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM.
The `onWillPatch` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillPatch(() => {
this.scrollState = this.getScrollSTate();
});
}
```
The `willPatch` is called in the usual parent->children order.
### `patched`
This hook is called whenever a component did actually update its DOM (most
likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the component is
not in the DOM.
The `onPatched` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onPatched(() => {
this.scrollState = this.getScrollSTate();
});
}
```
Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in
turn will cause other calls to the `patched` method. So, we need to be particularly
careful at avoiding endless cycles.
Like `mounted`, the `patched` hook is called in the order: children first, then
parent.
### `willUnmount`
`willUnmount` is a hook that is called each time just before a component is
unmounted from the DOM. This is a good place to remove listeners, for example.
The `onWillUnmount` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onMounted(() => {
// add some listener
});
onWillUnmount(() => {
// remove listener
});
}
```
This is the opposite method of `mounted`. Note that if a component is destroyed
before being mounted, the `willUnmount` method may not be called.
Parent `willUnmount` hooks will be called before children.
### `willDestroy`
Sometimes, components need to do some action in the `setup` and clean it up when
they are inactive. However, the `willUnmount` hook is not appropriate for the
cleaning operation, since the component may be destroyed before it has even been
mounted. The `willDestroy` hook is useful in that situation, since it is always
called.
The `onWillUnmount` hook is used to register a function that will be executed at
this moment:
```javascript
setup() {
onWillDestroy(() => {
// do some cleanup
});
}
```
The `willDestroy` hooks are first called on children, then on parents.
### `onError`
Sadly, it may happen that components crashes at runtime. This is an unfortunate
reality, and this is why Owl needs to provide a way to handle these errors.
The `onError` hook is useful when we need to intercept and properly react
to errors that occur in some sub components. See the page on
[error handling](error_handling.md) for more detail.
```javascript
setup() {
onError(() => {
// do something
});
}
```
## Sub components
It is convenient to define a component using other (sub) components. This is
called composition, and is very powerful in practice. To do that in Owl, one
can just use a tag starting with a capital letter in its template, and register
the sub component class in its static `components` object:
```js
class Child extends Component {
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child value="1"/>
<Child value="2"/>
</div>`;
static components = { Child };
}
```
This example also shows how one can pass information from the parent component
to the child component, as props. See the [props section](props.md)
for more information.
## Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values. This should
be an expression that evaluates to a component class. For example:
```js
class A extends Component {
static template = xml`<div>child a</div>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class Parent extends Component {
static template = xml`<t t-component="myComponent"/>`;
state = useState({ child: "a" });
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
## `status` helper
It is sometimes convenient to have a way to find out in which state a component
is currently. To do that, one can use the `status` helper:
```js
const { status } = owl;
// assume component is an instance of a Component
console.log(status(component));
// logs either:
// - 'new', if the component is new and has not been mounted yet
// - 'mounted', if the component is currently mounted
// - 'destroyed' if the component is currently destroyed
```
-163
View File
@@ -1,163 +0,0 @@
# 🦉 Concurrency Model 🦉
## Content
- [Overview](#overview)
- [Rendering Components](#rendering-components)
- [Semantics](#semantics)
- [Asynchronous Rendering](#asynchronous-rendering)
## Overview
Owl was designed from the very beginning with asynchronous components. This comes
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
asynchronous hooks, it is possible to build complex highly concurrent applications.
Owl concurrent mode has several benefits: it makes it possible to delay the
rendering until some asynchronous operation is complete, it makes it possible
to lazy load libraries, while keeping the previous screen completely functional.
It is also good for performance reasons: Owl uses it to only apply the result of
many different renderings only once in an animation frame. Owl can cancel
a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concurrent rendering works in a general way.
## Rendering Components
The word _rendering_ is a little vague, so, let us explain more precisely the
process by which Owl components are displayed on a screen.
When a component is mounted or updated, a new rendering is started. It has
two phases: _virtual rendering_ and _patching_.
### Virtual rendering
This phase represent the process of rendering a template, in memory, which creates
a virtual representation of the desired component html). The output of this phase is a
virtual DOM.
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
will need to be called), or updated (which is done with the `willUpdateProps`
method). This is completely a recursive process: a component is the root of a
component tree, and each sub component needs to be (virtually) rendered.
### Patching
Once a rendering is complete, it will be applied on the next animation frame.
This is done synchronously: the whole component tree is patched to the real
DOM.
## Semantics
We give here an informal description of the way components are created/updated
in an application. Here, ordered lists describe actions that are executed
sequentially, bullet lists describe actions that are executed in parallel.
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
```
A
/ \
B C
/ \
D E
```
Here is what happen whenever we mount the root
component (with some code like `app.mount(document.body)`).
1. `willStart` is called on `A`
2. when it is done, template `A` is rendered.
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. each components are patched into a detached DOM element, in the following order:
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
in one pass)
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all components in the following
order: `E`, `D`, `C`, `B`, `A`.
**Scenario 2: updating a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new component `F`.
So, the component tree should look like this:
```
A
/ \
B C
/ \
D F
```
Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `F` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. components `F`, `D` are patched in that order
5. component `C` is patched, which will cause recursively:
1. `willUnmount` hook on `E`
2. destruction of `E`,
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`.
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
There are two different common problems with Owl asynchronous rendering model:
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. 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
second, and only once.
-76
View File
@@ -1,76 +0,0 @@
# 🦉 Environment 🦉
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
## Overview
An environment is a shared object given to all components in a tree. It is not
used by Owl itself, but it is useful for application developers to provide a
simple communication channel between components (in addition to the props).
The `env` given to the [`App`](app.md) is assigned to the `env` component
property.
```
Root
/ \
A B
```
Also, the `env` object is frozen when the application is started. This is done
to ensure a simpler mental model of what's happening in runtime. Note that it
is only shallowly frozen, so sub objects can be modified.
## Setting an environment
The correct way to customize an environment is to simply give it to the `App`,
whenever it is created.
```js
const env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
new App(Root, { env }).mount(document.body);
// or alternatively
mount(App, document.body, { env });
```
## Using a sub environment
It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment.
There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
```js
class SomeComponent extends Component {
setup() {
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
}
}
```
## Content of an Environment
The `env` object content is totally up to the application developer. However,
some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs).
- other utility functions that one want to inject, such as a translation function.
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
-75
View File
@@ -1,75 +0,0 @@
# 🦉 Error Handling 🦉
## Content
- [Overview](#overview)
- [Managing Errors](#managing-errors)
- [Example](#example)
## Overview
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible feedback.
Clearly, it is usually a little bit extreme to destroy the application. This
is why we need a mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `onError` hook.
The main idea is that the `onError` hook register a function that will be called
with the error. This function need to handle the situation, most of the time by
updating some state and rerendering itself, so the application can return to a
normal state.
## Managing Errors
Whenever the `onError` lifecycle hook is used, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `onError` method. This allows us to properly handle the error, and to
not break the application.
There are important things to know:
- If an error that occured in the internal rendering cycle is not caught, then
Owl will destroy the full application. This is done on purpose, because Owl
cannot guarantee that the state is not corrupted from this point on.
- errors coming from event handlers are NOT managed by `onError` or any other
owl mechanism. This is up to the application developer to properly recover
from an error
- if an error handler is unable to properly handle an error, it can just rethrow
an error, and Owl will try looking for another error handler up the component
tree.
## Example
For example, here is how we could implement a generic component `ErrorBoundary`
that render its content, and a fallback if an error happened.
```js
class ErrorBoundary extends Component {
static template = xml`
<t t-if="error" t-slot="fallback">An error occurred</t>
<t t-else="" t-slot="content"`;
setup() {
this.state = useState({ error: false });
onError(() => (this.state.error = true));
}
}
```
Using the `ErrorBoundary` is then simple simple:
```xml
<ErrorBoundary>
<SomeOtherComponent/>
<t t-set-slot="fallback">Some specific error message</t>
</ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
-119
View File
@@ -1,119 +0,0 @@
# 🦉 Event Handling 🦉
## Content
- [Event Handling](#event-handling)
- [Modifiers](#modifiers)
- [Synthetic Events](#synthetic-events)
- [On Components](#on-components)
## Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. This
is done with the `t-on` directive. For example:
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event. The value of the `t-on` expression should be a valid javascript expression
that evaluates to a function in the context of the current component. So, one
can get a reference to the event, or pass some additional arguments. For example,
all the following expressions are valid:
```xml
<button t-on-click="someMethod">Do something</button>
<button t-on-click="() => this.increment(3)">Add 3</button>
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
```
Notice the use of the `this` keyword in the lambda function: this is the
correct way to call a method on the component in a lambda function.
One could use the following expression:
```xml
<button t-on-click="() => increment(3)">Add 3</button>
```
But then, the increment function may be unbound (unless the component binds it
in its setup function, for example).
## Modifiers
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
| `.synthetic` | define a synthetic event handler (see below) |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click.stop="">Do something</button>
```
This will simply stop the propagation of the event.
## Synthetic Events
In some cases, attaching an event handler for each element of large lists has
a non trivial cost. Owl provides a way to efficiently improve the performance:
with synthetic event, it actually adds only one handler on the document body,
and will properly call the handler, just as expected.
The only difference with regular events is that the event is caught at the document
body, so it cannot be stopped before it actually gets there. Since it may be
surprising in some cases, it is not enabled by default.
To enable it, one can just use the `.synthetic` suffix:
```xml
<div>
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
<button t-on-click.synthetic="doSomething" ...>
<!-- some content -->
</button>
</t>
</div>
```
## On Components
The `t-on` directive also works on a child component:
```xml
<div>
in some template
<Child t-on-click="dosomething"/>
</div>
```
This will catch all click events on any html element contained in the `Child`
sub component. Note that if the child component is reduced to one (or more) text
nodes, then clicking on it will not call the handler, since the event will be
dispatched by the browser on the parent element (a `div` in this case).
-362
View File
@@ -1,362 +0,0 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [The Hook Rule](#the-hook-rule)
- [Lifecycle hooks](#lifecycle-hooks)
- [Other hooks](#other-hooks)
- [`useState`](#usestate)
- [`useRef`](#useref)
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
- [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [`useEffect`](#useeffect)
- [`useRoots`](#useroots)
- [Example: Mouse Position](#example-mouse-position)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks work beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## The Hook Rule
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
```js
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
## Lifecycle Hooks
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
| Hook | Description |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| **[onWillStart](component.md#willstart)** | async, before first rendering |
| **[onWillRender](component.md#willrender)** | just before component is rendered |
| **[onRendered](component.md#rendered)** | just after component is rendered |
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
| **[onPatched](component.md#patched)** | just after the DOM is patched |
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
## Other Hooks
### `useState`
The `useState` hook is certainly the most important hook for Owl components:
this is what allows a component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState, Component } = owl;
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++;
}
}
```
It is important to remember that `useState` only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It only work on a html element tagged by the
`t-ref` directive:
```xml
<div>
<input t-ref="someDiv"/>
<span>hello</span>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` with the `useRef` hook:
```js
class Parent extends Component {
inputRef = useRef("someComponent");
someMethod() {
// here, if component is mounted, refs are active:
// - this.inputRef.el is the input HTMLElement
}
}
```
As shown by the example above, the actual HTMLElement instance is accessed with
the `el` key.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](templates.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references need to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv` and `useChildSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application.
This is where the `useChildSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only its children
can access it:
```js
class FormComponent extends Component {
setup() {
const model = makeModel();
// model will be available on this.env for this component and all children
useSubEnv({ model });
// someKey will be available on this.env for all children
useChildSubEnv({ someKey: "value" });
}
}
```
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
contains some key/value that will be added to the current environment. These hooks
will create a new env object with the new information:
- `useSubEnv` will assign this new `env` to itself and to all children components
- `useChildSubEnv` will only assign this new `env` to all children components.
As usual in Owl, [environments](environment.md) created with these two hooks are
frozen, to prevent unwanted modifications.
Note that both these hooks can be called an arbitrary number of times. The `env`
will then be updated accordingly.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
```js
function useSomething() {
const component = useComponent();
// now, component is bound to the instance of the current component
}
```
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
```js
function useSomething() {
const env = useEnv();
// now, env is bound to the env of the current component
}
```
### `useEffect`
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 (only if some dependencies have changed).
It has almost the same API as the React `useEffect` hook, except that the dependencies
are defined by a function instead of just the dependencies.
The `useEffect` hook takes two function: the effect function and the dependency
function. The effect function perform some task and return (optionally) a cleanup
function. The dependency function returns a list of dependencies, these dependencies
are passed as parameters in the effect function . If any of these
dependencies changes, then the current effect will be cleaned up and reexecuted.
Here is an example without any dependencies:
```js
useEffect(
() => {
window.addEventListener("mousemove", someHandler);
return () => window.removeEventListener("mousemove", someHandler);
},
() => []
);
```
In the example above, the dependency list is empty, so the effect is only cleaned
up when the component is unmounted.
If the dependency function is skipped, then the effect will be cleaned up and
rerun at every patch.
Here is another example, of how one could implement a `useAutofocus` hook with
the `useEffect` hook:
```js
function useAutofocus(name) {
let ref = useRef(name);
useEffect(
(el) => el && el.focus(),
() => [ref.el]
);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
setup() {
useAutofocus("myinput");
}
}
```
### `useRoots`
`useRoots` is an alternative way to get a reference to the root notes or elements
of a component. It may be useful in some cases where `useRef` cannot be applied,
such as a higher order component, or a component with only text as content.
The return value of `useRoots` is an object with the following key/values:
- `node`: getter that evaluates to the first node of the component content (or null)
- `elem`: getter that evaluates to the first HTMLElement of the component content (or null)
- `nodes`: iterator that returns all content nodes
- `elems`: iterator that returns all HTMLElement nodes
```js
class Child extends Component {
static template = xml`<p>some content</p>`;
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
setup() {
this.roots = useRoots();
console.log(this.roots.elem); // null
onMounted(() => {
console.log(this.roots.elem); // log the `p` element from the child
for (let elem of this.roots.elems) {
console.log(elem); // log the `p` element from the child
}
});
}
}
```
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onWillDestroy, Component } = owl;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener("mousemove", update);
onWillDestroy(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class Root extends Component {
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
-92
View File
@@ -1,92 +0,0 @@
# 🦉 Form Input Bindings 🦉
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts the following modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
-17
View File
@@ -1,17 +0,0 @@
# 🦉 Portal 🦉
It is sometimes useful to be able to render some content outside the boundaries
of a component. To do that, Owl provides a special directive: `t-portal`:
```js
class SomeComponent extends Component {
static template = xml`
<div>this is inside the component</div>
<div t-portal="'body'">and this is outside</div>
`;
}
```
The `t-portal` directive takes a valid css selector as argument. The content of
the portalled template will be mounted at the corresponding location. Note that
Owl need to insert an empty text node at the location of the portalled content.
-30
View File
@@ -1,30 +0,0 @@
# 🦉 Precompiling templates 🦉
Owl is designed to be used by the Odoo javascript framework. Since Odoo handles
its assets in its own non standard way, it was decided/assumed that Owl would
compile templates at runtime.
However, in some cases, it is not optimal, or even worse, not possible to do that.
For example, browser extensions do not allow javascript code to create a new
function (using the `new Function(...)` syntax).
Therefore, in these cases, it is required to compile templates ahead of time. It
is possible to do that in Owl, but the tooling is still rough. For now, the
process is the following:
1. write your templates in xml files (with a `t-name` directive to declare the name
of the template)
2. Compile them in a `templates.js` file
3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler)
4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to
be positioned before the templates)
Here is a more detailed explanation on how to compile xml files into a js file:
1. clone the owl repository locally
2. `npm install` to install all the required tooling
3. `npm run build:runtime` to build the `owl.iife.runtime.js` file
4. `npm run build:compiler` to build the template compiler
5. `npm run compile_templates -- path/to/your/templates` will scan your target
folder, find all xml files, get all templates, compile them, and generate a
`templates.js` file.
-270
View File
@@ -1,270 +0,0 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Binding function props](#binding-function-props)
- [Dynamic Props](#dynamic-props)
- [Default Props](#default-props)
- [Props validation](#props-validation)
- [Good Practices](#good-practices)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
## Binding function props
It is common to have the need to pass a callback as a prop. Since Owl components
are class based, the callback frequently needs to be bound to its owner component.
So, one can do this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback="doSomething"/>
</div>`;
setup() {
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// ...
}
}
```
However, this is such a common use case that Owl provides a special suffix to do
just that: `.bind`. This looks like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback.bind="doSomething"/>
</div>`;
doSomething() {
// ...
}
}
```
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
## Default Props
If the static `defaultProps` property is defined, it will be used to complete
props received by the parent, if missing.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
...
}
```
In the example above, the `initialValue` props is now by default set to 0.
## Props Validation
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [how to configure an app](app.md#configuration))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error (unless the special prop `*` is present).
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, it is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- an object describing a value as type. This is done by using the `value` key. For example, `{value: false}` specifies that the corresponding value should be equal to false.
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory
There is a special `*` prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their
props to their child components.
Note that default values cannot be defined for a mandatory props. Doing so will
result in a prop validation error.
Examples:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
};
...
}
```
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// only the existence of those 3 keys is documented. any other key is allowed.
static props = ['message', 'id', 'date', '*'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
someId: [Number, {value: false}], // either a number or false
};
```
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
-131
View File
@@ -1,131 +0,0 @@
# 🦉 Reactivity 🦉
## Content
- [Overview](#overview)
- [`useState`](#usestate)
- [`reactive`](#reactive)
- [`markRaw`](#markraw)
- [`toRaw`](#toraw)
## Overview
Reactivity is a big topic in javascript frameworks. The goal is to provide a
simple way to manipulate state, in such a way that the interface automatically
update accordingly to state changes. Also, we obviously want this to happen in
a performant way.
To solve this issue, Owl provides two reactivity primitives:
- `reactive`, which returns a proxy to its first argument, and tracks all read/update
operation going through it,
- `useState`: a hook, that internally uses `reactive`, and is linked to its
owner component: any read operation will be tracked (key by key), and any
updates to these tracked values will cause the component to be rerendered.
Most of the time, the `useState` hook is the best solution.
Since version 2.0, Owl applies the fine grained reactivity at the component
level: reactive objects received as props are automatically subscribed to by the
component, so Owl can track which part of these props are consumed by each
component, and is therefore able to only rerender the impacted components.
## `useState`
Let us start by an example of how `useState` could be used:
```js
class Counter extends Component {
static template = xml`
<div t-on-click="increment">
<t t-esc="state.value"/>
</div>`;
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
```
If one were to use a simple state object, Owl would not be aware that the value
was changed and that the component should be rerendered. With the `useState`
hook, `this.state` is now a reactive object, so this component works as expected.
## `reactive`
The `reactive` function is the basic reactivity primitive. It takes an object
or an array as first argument, and optionally, a function as the second argument.
The function will be called whenever any tracked value is updated.
```js
const obj = reactive({ a: 1 }, () => console.log("changed"));
obj.a = 2; // does not log anything: the 'a' key was not read
console.log(obj.a); // log 2, and reads the 'a' key => it is now tracked
obj.a = 3; // log 'changed' because we updated a tracked value
```
An important property of reactive objects is that they can be reobserved: this
will create an independant proxy that tracks another set of keys:
```js
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
const obj2 = reactive(obj1, () => console.log("observer 2"));
console.log(obj1.a); // log 1, and reads the 'a' key => it is now tracked by observer 1
console.log(obj1.b); // log 2, and 'b' is now tracked by observer 1
console.log(obj2.b); // log 2, and 'b' is now tracked by observer 1
obj2.a = 3; // log 'observer1', because observer2 does not track a
obj2.b = 3; // log 'observer1' and 'observer2'
```
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
is the proper way to watch for some state changes.
## `markRaw`
Marks an object so that it is ignored by the reactivity system. This function returns its argument.
```js
const someObject = markRaw(...);
const state = useState({
a: 1,
obj: someObject
});
// here, state.obj === someObject
```
This is useful in some rare cases. For example, some complex and large object such
that going through the reactivity system may cause a non trivial performance slowdown.
However, use this function with caution: this is an escape hatch from the reactivity
system, and as such, using it may cause subtle and unintended issues!
## `toRaw`
Given a reactive object, this function returns the underlying, non-reactive,
corresponding object.
```js
// in setup
const state = useState({ value: 1 });
// later:
const rawState = toRaw(this.state);
rawState.value = 3; // will NOT be picked up by the reactivity system!!!
```
Here again, this is useful in some situations where we want to explicitely bypass
Owl, but using this function means that the responsability of coordinating
state update is given to the user code, instead of Owl. Subtle bugs may arise!
Also, normal (non-reactive objects) will be directly returned by `toRaw`:
```js
const obj = { a: 1 };
console.log(toRaw(obj) === obj); // true
```
-37
View File
@@ -1,37 +0,0 @@
# 🦉 References 🦉
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml
<div>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
```js
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
Be aware that the `el` property will only be set when the target of the `t-ref`
directive is mounted in the DOM. Otherwise, it will be set to `null`.
The `useRef` hook cannot be used to get a reference to an instance of a sub
component.
Note that this example uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget that it is
a reference object.
-260
View File
@@ -1,260 +0,0 @@
# 🦉 Slots 🦉
## Content
- [Overview](#overview)
- [Named slots](#named-slots)
- [Rendering Context](#rendering-context)
- [Default Slot](#default-slot)
- [Default Content](#default-content)
- [Dynamic slots](#dynamic-slots)
- [Slots and props](#slots-and-props)
- [Slot params](#slot-params)
- [Slot scopes](#slot-scopes)
## Overview
Owl is a template based component system. There is therefore a need to be able
to make generic components. For example, imagine a generic `Navbar`
component, which displays a navbar, but with some customizable content. Since
the specific content is only known to the user of the `Navbar`, it would be nice
to specify it in the template where `Navbar` is used:
```xml
<div>
<Navbar>
<span>Hello Owl</span>
</Navbar>
</div>
```
This is exactly the way slots work! In the example above, the user of the `Navbar`
component specify some content (here, in the default slot). The `Navbar`
component can insert that content in its own template at the appropriate location.
An important information to notice is that the content of the slot is rendered in
the parent context, not in the navbar. As such, it can access values and methods
from the parent component.
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
```xml
<div class="navbar">
<t t-slot="default"/>
<ul>
<!-- rest of the navbar here -->
</ul>
</div>
```
## Named slots
Default slots are very useful, but sometimes, we may need more than one slot.
This is what named slots are for! For example, suppose we implement a component
`InfoBox` that display a title and some specific content. Its template could look
like this:
```xml
<div class="info-box">
<div class="info-box-title">
<t t-slot="title"/>
<span class="info-box-close-button" t-on-click="close">X</span>
</div>
<div class="info-box-content">
<t t-slot="content"/>
</div>
</div>
```
And one could use it with the `t-set-slot` directive:
```xml
<InfoBox>
<t t-set-slot="title">
Specific Title. It could be html also.
</t>
<t t-set-slot="content">
<!-- some template here, with html, events, whatever -->
</t>
</InfoBox>
```
## Rendering context
The content of the slots is actually rendered with the 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 to the correct component (usually, the
grandparent of the slot content).
## Default Slot
All elements inside the component which are not a named slot will be treated as
part of the content of the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
One can mix default slot and named slots:
```xml
<div>
<Child>
default content
<t t-set-slot="footer">
content for footer slot here
</t>
</Child>
</div>
```
## Default content
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
## Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
This will evaluate the `current` expression, and insert the corresponding slot
at the place of the `t-slot` directive.
## Slots and props
In a sense, slots are almost the same as a prop: they define some information
to pass to the child component. To make it possible to use it, and to pass it
down to sub component, Owl actually define a special prop `slots` that contains
all slot information given to the component. It looks like this:
```js
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
```
So, a component can pass its slots to a subcomponent like this:
```xml
<Child slots="props.slots"/>
```
## Slot params
For advanced usecases, it may be necessary to pass additional information to a
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
directive. Then, the generic component can read them in its prop `slots`.
For example, here is how a Notebook component could be implemented (a component
with multiple page, and a tab bar, which only render the current active page,
and each page has a title).
```js
class Notebook extends Component {
static template = xml`
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
</div>
<div class="page">
<t t-slot="{{currentSlot}}"/>
</div>
</div>`;
setup() {
this.state = useState({ activeTab: 0 });
this.tabNames = Object.keys(this.props.slots);
}
get currentSlot() {
return this.tabNames[this.state.activeTab];
}
}
```
Notice how one can read the `title` value for each slots. Here is how one could
use this `Notebook` component:
```xml
<Notebook>
<t t-set-slot="page1" title="'Page 1'">
<div>this is in the page 1</div>
</t>
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
<div>this is in the page 2</div>
</t>
</Notebook>
```
Slot params works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
## Slot scopes
For other kinds of advanced use cases, the content of a slot may depends on some
information specific to the generic component. This is the opposite of the slot
params.
To solve this kind of problems, one can use the `t-slot-scope` directive along
with the `t-set-slot`. This defines the name of a variable that can access
everything given by the child component:
```xml
<MyComponent>
<t t-set-slot="foo" t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</t>
</MyComponent>
```
And the child component that includes the slot can provide values like this:
```xml
<t t-slot="foo" bool="other_var" num="5">
```
or this:
```xml
<t t-slot="foo" t-props="someObject">
```
In the case of the default slot, you may declare the slot scope directly on the
component itself:
```xml
<MyComponent t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</MyComponent>
```
Slot values works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
-703
View File
@@ -1,703 +0,0 @@
# 🦉 Templates 🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Template reference](#qweb-template-reference)
- [White Spaces](#white-spaces)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Sub Templates](#sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Debugging](#debugging)
- [Fragments](#fragments)
- [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg)
- [Restrictions](#restrictions)
## Overview
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML. Also, since Owl is a live
component system, there are additional directives specific to Owl (such as `t-on`).
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="">
<li t-foreach="messages" t-as="message">
<t t-esc="message"/>
</li>
</ul>
</div>
```
Template directives are specified as XML attributes prefixed with `t-`, for
instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which
executes its directive but doesnt generate any output in and of itself.
We present in this section the templating language, including its Owl specific
extensions.
## Directives
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | --------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| -------------------------------------- | --------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#sub-components) |
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](event_handling.md) |
| `t-portal` | [Portal](portal.md) |
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](input_bindings.md) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## QWeb Template Reference
### White Spaces
White spaces in a template are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced with a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that apply on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context (typically, the component):
```xml
<p t-if="user.birthday === today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | replaced with |
| ----- | ------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves:
```xml
<div>hello</div> <!–– rendered as itself ––>
```
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
```xml
<p><t t-esc="value"/></p>
```
rendered with the value `value` set to `42` in the rendering context yields:
```html
<p>42</p>
```
The `t-out` directive is almost the same as `t-esc`, but possibly without the
escaping. The difference is that the value received by the `t-out` directive
will only be not-escaped if it has been marked as such, using the `markup`
utility function:
For example, in the following component:
```js
const { markup, Component, xml } = owl;
class SomeComponent extends Component {
static template = xml`
<t t-out="value1"/>
<t t-out="value2"/>`;
value1 = "<div>some text 1</div>";
value2 = markup("<div>some text 2</div>");
}
```
The first `t-out` will act as a `t-esc` directive, which means that the content
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
this will be injected as html.
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
1. a `t-value` attribute containing an expression, and the result of its
evaluation will be set:
```xml
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
```
will print `3`. Note that the evaluation is done at rendering time, not at
compilte time.
2. if there is no `t-value` attribute, the nodes body is saved and its value is
set as the variables value:
```xml
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
```
will generate `&lt;li&gt;ok&lt;/li&gt;` (the content is escaped as we used the `t-esc` directive)
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
```xml
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
```
The element is rendered if the condition (evaluated with the current rendering
context) is true:
```xml
<div>
<p>ok</p>
</div>
```
but if the condition is false it is removed from the result:
```xml
<div>
</div>
```
The conditional rendering applies to the bearer of the directive, which does not
have to be `<t>`:
```xml
<div>
<p t-if="condition">ok</p>
</div>
```
will give the same results as the previous example.
Extra conditional branching directives `t-elif` and `t-else` are also available:
```xml
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
```
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
```xml
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
It is sometimes convenient to format an attribute with string interpolation. In
that case, the `t-attf-` directive can be used. It is useful when we need to mix
literal and dynamic elements, such as css classes. The dynamic elements can be
specified with either `{{...}}` or `#{...}`:
```xml
<div t-attf-foo="a {{value1}} is #{value2} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
If we need completely dynamic attribute names, then there is an additional
directive: `t-att`, which takes either an object (with keys mapping to their
values) or a pair `[key, value]`. For example:
```xml
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></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
QWeb has an iteration directive `t-foreach` which take an expression returning the
collection to iterate on, and a second parameter `t-as` providing the name to use
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<p><t t-esc="i"/></p>
</t>
```
will be rendered as:
```xml
<p>1</p>
<p>2</p>
<p>3</p>
```
Like conditions, `t-foreach` applies to the element bearing the directives attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile
renderings.
`t-foreach` can iterate on an array (the current item will be the current value)
or an object (the current item will be the current key).
In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced with the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratees size be
available
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i" t-key="i">
<t t-set="existing_variable" t-value="true"/>
<t t-set="new_variable" t-value="true"/>
<!-- existing_variable and new_variable now true -->
</p>
<!-- existing_variable always true -->
<!-- new_variable undefined -->
```
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to make sure we keep the actual DOM node instead of replacing it with
a new one.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item" t-key="item_index"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
If there is no `t-key` directive, Owl will use the index as a default key.
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:
```xml
<t t-foreach="...items" t-as="item">...</t>
```
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
templates), using the `t-call` directive:
```xml
<div t-name="other-template">
<p><t t-value="var"/></p>
</div>
<div t-name="main-template">
<t t-set="var" t-value="owl"/>
<t t-call="other-template"/>
</div>
```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape.
Sometimes, one might want to pass information to the sub template. In that case,
the content of the body of the `t-call` directive is available as a special
magic variable `0`:
```xml
<t t-name="other-template">
This template was called with content:
<t t-raw="0"/>
</t>
<div t-name="main-template">
<t t-call="other-template">
<em>content</em>
</t>
</div>
```
will result in :
```xml
<div>
This template was called with content:
<em>content</em>
</div>
```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
Note: by default, the rendering context for a sub template is simply the current
rendering context (so, the current component). However, it may be useful to be
able to specify a specific object as context. This can be done by using the
`t-call-context` directive:
```xml
<t t-call="other-template" t-call-context="obj"/>
```
### Dynamic sub templates
The `t-call` directive can also be used to dynamically call a sub template,
using string interpolation. For example:
```xml
<div t-name="main-template">
<t t-call="{{template}}">
<em>content</em>
</t>
</div>
```
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
`t-debug` adds a debugger statement during template rendering:
```xml
<t t-if="a_test">
<t t-debug=""/>
</t>
```
will stop execution if the browser dev tools are open.
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
```xml
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
```
will print 42 to the console.
## Fragments
Owl 2 supports templates with an arbitrary number of root elements, or even just
a text node. So, the following templates are all valid:
```xml
hello owl. This is just a text node!
```
```xml
<div>hello</div>
```
```xml
<div>hello</div>
<div>ola</div>
```
```xml
<div t-if="someCondition"><SomeChildComponent/></div>
```
```xml
<t t-if="someCondition"><SomeChildComponent/></t>
```
## Inline templates
Most real applications will define their templates in a XML file, to benefit
from the XML ecosystem, and to do some additional processing, such as translating
them. However, in some cases, it is convenient to be able to define a template
inline. To do so, one can use the `xml` helper function:
```js
const { Component, xml } = owl;
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
mount(MyComponent, document.body);
```
This function simply generates an unique string id, and register the template
under that id in the internals of Owl, then return the id.
## Rendering svg
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] },
],
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
**Important note:** Owl needs to properly set the namespace for each svg elements.
Since Owl compile each template separately, it is not able to determine easily
if a template is supposed to be included in a svg namespace or not. Therefore,
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
be considered as svg. In practice, this means that each component or each sub
templates (included with `t-call`) should have one of these tag as root tag.
## Restrictions
Note that Owl templates forbid the use of tag and or attributes starting with
the `block-` string. This restriction prevents name collision with the internal
code of Owl.
```xml
<div><block-1>this will not be accepted by Owl</block-1></div>
```
-70
View File
@@ -1,70 +0,0 @@
# 🦉 Translations 🦉
If properly setup, Owl can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non",
};
const translateFn = (str) => translations[str] || str;
const app = new App(Root, { templates, tranaslateFn });
// ...
```
See the [app configuration page](app.md#configuration) for more info on how to
configure an Owl application.
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
In some case, it is useful to be able to extend the list of translatable attributes.
For example, one may want to also translate `data-title` attributes. To do that,
we can define additional attributes with the `translatableAttributes` option:
```js
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
// ...
```
It is also possible to remove an attribute from the default list by prefixing it with `-`:
```js
const app = new App(Root, {
templates,
tranaslateFn,
translatableAttributes: ["data-title", "-title"],
});
// data-title attribute will be translated, but not title attribute...
```
-80
View File
@@ -1,80 +0,0 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadFile`](#loadfile): loading a file (useful for templates)
- [`EventBus`](#eventbus): a simple EventBus
- [`validate`](#validate): a validation function
## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
const { whenReady } = owl;
await whenReady();
// do something
```
or alternatively:
```js
whenReady(function () {
// do something
});
```
## `loadFile`
`loadFile` is a helper function to fetch a file. It simply
performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example:
```js
const { loadFile } = owl;
async function makeEnv() {
const templates = await loadFile("templates.xml");
// do something
}
```
## `EventBus`
It is a simple `EventBus`, with the same API as usual DOM elements, and an
additional `trigger` method to dispatch events:
```js
const bus = new EventBus();
bus.addEventListener("event", () => console.log("something happened"));
bus.trigger("event"); // 'something happened' is logged
```
## `validate`
The `validate` function is a function that validates if a given object satisfies a
specified schema. It is actually used by Owl itself to perform
[props validation](props.md#props-validation). For example:
```js
validate(
{ a: "hey" },
{
id: Number,
url: [Boolean, { type: Array, element: Number }],
}
);
// throws an error with the following information:
// - unknown key 'a',
// - 'id' is missing (should be a number),
// - 'url' is missing (should be a boolean or list of numbers),
```
+171
View File
@@ -0,0 +1,171 @@
# 🦉 Router 🦉
## 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;
}
```
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`).
```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>
```
+289
View File
@@ -0,0 +1,289 @@
# 🦉 Store 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [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 part 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 state, and let the developer update it in a structured way, with `actions`.
Owl components can then connect to the store, and will be updated if necessary.
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", () => 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";
}
}
};
```
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 partial 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 cached if they don't take any argument, or their argument
is a string or a number.
### Connecting a Component
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The `ConnectedComponent` class can be configured with the following fields:
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict.
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)
Note that the class `ConnectedComponent` has a `dispatch` method. This means
that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
</button>
```
### Semantics
The `Store` and the `ConnectedComponent` 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 other parts 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 `mapStoreToProps` and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` 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.
+47
View File
@@ -0,0 +1,47 @@
# 🦉 Tags 🦉
Tags are very small helper to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write single file components.
## XML tag
Without tags, creating a standalone component would look like this:
```js
import { Component } from 'owl'
const name = 'some-unique-name';
const template = `
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
QWeb.registerTemplate(name, template);
class MyComponent extends Component {
static template = name;
...
}
```
With tags, this process is slightly simplified. The name is uniquely generated,
and the template is automatically registered:
```js
import { Component } from 'owl'
import { xml } from 'owl/tags'
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
```
+87
View File
@@ -0,0 +1,87 @@
# 🦉 Tooling 🦉
## Content
- [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
## Overview
To help work with/improve/learn OWL, there are a few extras tools/settings.
- development mode: enable better error reporting for the developer
- a playground application: a space to experiment and learn Owl.
- a benchmarks application: allow comparison with a few common frameworks
The two applications are available in the `tools/` folder, and can be accessed
by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground
The playground is an important application designed to help learning and
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
It is an application similar to `jsFiddle`, but specialized for Owl: there are
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
code in an iframe.
## Benchmarks
Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks.
## Single File Component
If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template`
which, if installed, does exactly that. To enable it, you need to add a comment,
like this:
```js
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
static template = TEMPLATE;
static components = { Sidebar, Content };
// rest of component...
}
```
+63
View File
@@ -0,0 +1,63 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadTemplates`](#loadtemplates): loading xml files
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
});
```
```js
owl.utils.whenReady(function() {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
});
```
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
a promise, so the caller can properly react when it is ready. Also, it is smart:
it maintains a list of urls previously loaded (or currently being loaded), and
prevent doing twice the work.
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
## `loadTemplates`
```js
async function makeEnv() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
return { qweb };
}
```
## `escape`
## `debounce`
+18
View File
@@ -0,0 +1,18 @@
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
+22 -47
View File
@@ -1,77 +1,53 @@
{
"name": "@odoo/owl",
"version": "2.0.0-beta-20",
"name": "owl-framework",
"version": "0.22.0",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
"types": "dist/types/owl.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=12.18.3"
},
"main": "src/index.ts",
"scripts": {
"build:bundle": "rollup -c --failAfterWarnings",
"build:runtime": "rollup -c --failAfterWarnings runtime",
"build:compiler": "rollup -c --failAfterWarnings compiler",
"build": "npm run build:bundle",
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle",
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/server.py || python tools/server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"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",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
"tools:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/odoo/owl.git"
},
"author": "Odoo",
"license": "LGPL-3.0-only",
"license": "LGPL",
"bugs": {
"url": "https://github.com/odoo/owl/issues"
},
"homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/node": "^14.11.8",
"chalk": "^3.0.0",
"@types/jest": "^23.3.12",
"cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "2.4.1",
"rollup": "^2.56.3",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.1",
"rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^27.0.5",
"typescript": "4.5.2",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2",
"uglify-es": "^3.3.9"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
@@ -87,7 +63,6 @@
]
},
"prettier": {
"printWidth": 100,
"endOfLine": "auto"
"printWidth": 100
}
}
-9
View File
@@ -1,9 +0,0 @@
# 🦉 OWL Roadmap 🦉
- Current version: 2.X
- Status: stable
Owl is currently stable. No (large) improvements is expected in the near future.
Note that we intend to keep maintaining owl, and as such, improvements and/or
breaking changes may require a version bump in the future.
+10 -90
View File
@@ -1,94 +1,14 @@
import pkg from "./package.json";
import { version } from "./package.json";
import git from "git-rev-sync";
import typescript from 'rollup-plugin-typescript2';
import { terser } from "rollup-plugin-terser";
import dts from "rollup-plugin-dts";
let input, output;
const IIFE_FILENAME = "dist/owl.iife.js";
const CJS_FILENAME = "dist/owl.cjs.js";
const ES_FILENAME = "dist/owl.es.js";
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
throw new Error("package.json has been modified. Build script should be updated accordingly");
}
const outro = `
__info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}';
__info__.hash = '${git.short()}';
__info__.url = 'https://github.com/odoo/owl';
`;
switch (process.argv[4]) {
case "compiler":
input = "src/compiler/index.ts",
output = [
getConfigForFormat('cjs', 'dist/compiler.js', ''),
]
break;
case "runtime":
input = "src/runtime/index.ts";
output = [
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
]
break;
default:
input = "src/index.ts",
output = [
getConfigForFormat('esm', ES_FILENAME, outro),
getConfigForFormat('cjs', CJS_FILENAME, outro),
getConfigForFormat('iife', IIFE_FILENAME, outro),
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
]
}
/**
* Generate from a string depicting a path a new path for the minified version.
* @param {string} pkgFileName file name
*/
function addSuffix(pkgFileName, suffix) {
const parts = pkgFileName.split('.');
parts.splice(parts.length - 1, 0, suffix);
return parts.join('.');
}
/**
* Get the rollup config based on the arguments
* @param {string} format format of the bundle
* @param {string} generatedFileName generated file name
* @param {boolean} minified should it be minified
*/
function getConfigForFormat(format, generatedFileName, outro, minified = false) {
return {
file: minified ? addSuffix(generatedFileName, "min") : generatedFileName,
format: format,
// rollup.config.js
export default {
input: "dist/owl/index.js",
output: {
file: "dist/owl.js",
format: "iife",
name: "owl",
extend: true,
outro: outro,
freeze: false,
plugins: minified ? [terser()] : [],
indent: ' ', // indent with 4 spaces
};
}
export default [
{
input,
output,
plugins: [
typescript({
useTsconfigDeclarationDir: true
}),
]
},
{
input: "dist/types/index.d.ts",
output: [{ file: "dist/types/owl.d.ts", format: "es" }],
plugins: [dts()],
},
];
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
}
};
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
import type { TemplateSet } from "../runtime/template_set";
import type { BDom } from "../runtime/blockdom";
import { CodeGenerator, Config } from "./code_generator";
import { parse } from "./parser";
export type Template = (context: any, vnode: any, key?: string) => BDom;
export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Template;
interface CompileOptions extends Config {
name?: string;
}
export function compile(
template: string | Element,
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("app, bdom, helpers", code) as TemplateFunction;
}
-375
View File
@@ -1,375 +0,0 @@
import { OwlError } from "../runtime/error_handling";
/**
* Owl QWeb Expression Parser
*
* Owl needs in various contexts to be able to understand the structure of a
* string representing a javascript expression. The usual goal is to be able
* to rewrite some variables. For example, if a template has
*
* ```xml
* <t t-if="computeSomething({val: state.val})">...</t>
* ```
*
* this needs to be translated in something like this:
*
* ```js
* if (context["computeSomething"]({val: context["state"].val})) { ... }
* ```
*
* This file contains the implementation of an extremely naive tokenizer/parser
* and evaluator for javascript expressions. The supported grammar is basically
* only expressive enough to understand the shape of objects, of arrays, and
* various operators.
*/
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
});
export interface QWebVar {
id: string; // foo
expr: string; // scope.foo (local variables => only foo)
value?: string; // 1 + 3
hasBody?: boolean;
}
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
type TKind =
| "LEFT_BRACE"
| "RIGHT_BRACE"
| "LEFT_BRACKET"
| "RIGHT_BRACKET"
| "LEFT_PAREN"
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "TEMPLATE_STRING"
| "SYMBOL"
| "OPERATOR"
| "COLON";
interface Token {
type: TKind;
value: string;
originalValue?: string;
size?: number;
varName?: string;
replace?: Function;
isLocal?: boolean;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
"]": "RIGHT_BRACKET",
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN",
});
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof. Currently we don't support delete and void
const OPERATORS =
"...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function (expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"' && s !== "`") {
return false;
}
let i = 1;
let cur;
while (expr[i] && expr[i] !== start) {
cur = expr[i];
s += cur;
if (cur === "\\") {
i++;
cur = expr[i];
if (!cur) {
throw new OwlError("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new OwlError("Invalid expression");
}
s += start;
if (start === "`") {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer: any) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
},
};
}
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function (expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
let i = 1;
while (expr[i] && expr[i].match(/[0-9]|\./)) {
s += expr[i];
i++;
}
return { type: "VALUE", value: s };
} else {
return false;
}
};
let tokenizeSymbol: Tokenizer = function (expr) {
let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1;
while (expr[i] && expr[i].match(/\w/)) {
s += expr[i];
i++;
}
if (s in WORD_REPLACEMENT) {
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
}
return { type: "SYMBOL", value: s };
} else {
return false;
}
};
const tokenizeStatic: Tokenizer = function (expr) {
const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
}
return false;
};
const tokenizeOperator: Tokenizer = function (expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
}
}
return false;
};
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeOperator,
tokenizeSymbol,
tokenizeStatic,
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
* example: `tokenize("1 + b")` will return:
* ```js
* [
* {type: "VALUE", value: "1"},
* {type: "OPERATOR", value: "+"},
* {type: "SYMBOL", value: "b"}
* ]
* ```
*/
export function tokenize(expr: string): Token[] {
const result: Token[] = [];
let token: boolean | Token = true;
let error: any;
let current = expr;
try {
while (token) {
current = current.trim();
if (current) {
for (let tokenizer of TOKENIZERS) {
token = tokenizer(current);
if (token) {
result.push(token);
current = current.slice(token.size || token.value.length);
break;
}
}
} else {
token = false;
}
}
} catch (e) {
error = e; // Silence all errors and throw a generic error below
}
if (current.length || error) {
throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
}
return result;
}
//------------------------------------------------------------------------------
// Expression "evaluator"
//------------------------------------------------------------------------------
const isLeftSeparator = (token: Token) =>
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token: Token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
* proper lookups in the context.
*
* Usually, this kind of code would be very simple to do if we had an AST (so,
* if we had a javascript parser), since then, we would only need to find the
* variables and replace them. However, a parser is more complicated, and there
* are no standard builtin parser API.
*
* Since this method is applied to simple javasript expressions, and the work to
* be done is actually quite simple, we actually can get away with not using a
* parser, which helps with the code size.
*
* Here is the heuristic used by this method to determine if a token is a
* variable:
* - by default, all symbols are considered a variable
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*
* Some specific code is also required to support arrow functions. If we detect
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExprToArray(expr: string): Token[] {
const localVars = new Set<string>();
const tokens = tokenize(expr);
let i = 0;
let stack = []; // to track last opening [ or {
while (i < tokens.length) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
if (prevToken) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
}
if (token.type === "TEMPLATE_STRING") {
token.value = token.replace!((expr: any) => compileExpr(expr));
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
}
} else {
localVars.add(token.value); //] = { id: token.value, expr: token.value };
}
}
if (isVar) {
token.varName = token.value;
if (!localVars.has(token.value)) {
token.originalValue = token.value;
token.value = `ctx['${token.value}']`;
}
}
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" && token.varName && localVars.has(token.value)) {
token.originalValue = token.value;
token.value = `_${token.value}`;
token.isLocal = true;
}
}
return tokens;
}
// Leading spaces are trimmed during tokenization, so they need to be added back for some values
const paddedValues = new Map([["in ", " in "]]);
export function compileExpr(expr: string): string {
return compileExprToArray(expr)
.map((t) => paddedValues.get(t.value) || t.value)
.join("");
}
export const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
export function replaceDynamicParts(s: string, replacer: (s: string) => string) {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
}
let r = s.replace(
INTERP_REGEXP,
(s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}"
);
return "`" + r + "`";
}
export function interpolate(s: string): string {
return replaceDynamicParts(s, compileExpr);
}
File diff suppressed because it is too large Load Diff
+723
View File
@@ -0,0 +1,723 @@
import { Observer } from "../core/observer";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import "./directive";
import "./props_validation";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env {
qweb: QWeb;
[key: string]: any;
}
/**
* Fibers are small abstractions designed to contain all the internal state
* associated to a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*/
export interface Fiber<Props> {
force: boolean;
rootFiber: Fiber<any> | null;
isCancelled: boolean;
scope: any;
vars: any;
patchQueue: Fiber<any>[];
component: Component<any, any>;
vnode: VNode | null;
props: Props;
promise: Promise<VNode> | null;
// handlers?: any;
// mountedHandlers?: any;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
vnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any> | null;
children: { [key: number]: Component<T, any> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
currentFiber: Fiber<Props> | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate | null;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>;
static template?: string | null = null;
static _template?: string | null = null;
static _current?: any | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
env: T;
props: Props;
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
} else {
this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
}
this.__owl__ = {
id: id,
vnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
currentFiber: null,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
observer: null,
render: null,
classObj: null,
refs: null
};
}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the component is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
* fast as possible.
*
* Note: this method should not be called manually.
*/
async willStart() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*/
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return;
}
const fiber = this.__createFiber(false, undefined, undefined, undefined);
if (!__owl__.vnode) {
fiber.promise = this.__prepareAndRender(fiber);
const vnode = await fiber.promise;
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
this.__applyPatchQueue(fiber);
}
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.isMounted) {
this.__callWillUnmount();
this.el!.remove();
}
}
/**
* The render method is the main entry point to render a component (once it
* is ready. This method is not initially called when the component is
* rendered the first time).
*
* This method will cause all its sub components to potentially rerender
* themselves. Note that `render` is not called if a component is updated via
* its props.
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
return;
}
const fiber = this.__createFiber(force, undefined, undefined, undefined);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
this.__applyPatchQueue(fiber);
}
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* This method is a helper to create a fiber element.
*/
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
* level destroyed component, for performance reasons).
*
* The job of this method is mostly to call willUnmount hooks, and to perform
* all necessary internal cleanup.
*
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp.__callMounted();
}
}
__owl__.isMounted = true;
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB()
}
} catch (e) {
errorHandler(e, this);
}
}
__callWillUnmount() {
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp.__callWillUnmount();
}
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(
nextProps: Props,
parentFiber: Fiber<any>,
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
await this.__render(fiber);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.promise = this.__prepareAndRender(fiber);
return fiber.promise;
}
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
const qweb = this.env.qweb;
let p = (<any>this).constructor;
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
} else {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
}
__owl__.render = qweb.render.bind(qweb, p._template);
return this.__render(fiber);
}
__render(fiber: Fiber<Props>): Promise<VNode> {
const __owl__ = this.__owl__;
const promises: Promise<void>[] = [];
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
fiber: fiber
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child component can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
// we applly here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode);
}
/**
* Only called by qweb t-component directive
*/
__mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
/**
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
*/
__applyPatchQueue(fiber: Fiber<Props>) {
const patchQueue = fiber.patchQueue;
let component: Component<any, any> = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
errorHandler(e, component);
}
}
}
//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, component: Component<any, any>) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent!;
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
}
}
+522
View File
@@ -0,0 +1,522 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/context";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive", "asyncroot"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
}
}
}
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
}
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? ctx.currentKey
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
: `String(-${componentID} - i)`
: String(componentID);
if (ctx.allowMultipleRoots) {
templateID = `"_slot_${templateID}"`;
}
if (key || ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let templateId${id} = ${templateID};`);
templateID = `templateId${id}`;
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
if (classAttr) {
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.inLoop) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
}
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
);
if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async || keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (async) {
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addIf(
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
);
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${componentID}`);
// new component
let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update component
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
+96
View File
@@ -0,0 +1,96 @@
import { QWeb } from "../qweb/index";
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* 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.
*/
QWeb.utils.validateProps = function(Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!props[propName]) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
}
}
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): 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
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
+79
View File
@@ -0,0 +1,79 @@
/**
* We define here a simple event bus: it can
* - emit events
* - add/remove listeners.
*
* This is a useful pattern of communication in many cases. For OWL, each
* components and stores are event buses.
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type Callback = (...args: any[]) => void;
export interface Subscription {
owner: any;
callback: Callback;
}
//------------------------------------------------------------------------------
// EventBus
//------------------------------------------------------------------------------
export class EventBus {
subscriptions: { [eventType: string]: Subscription[] } = {};
/**
* Add a listener for the 'eventType' events.
*
* Note that the 'owner' of this event can be anything, but will more likely
* be a component or a class. The idea is that the callback will be called with
* the proper owner bound.
*
* Also, the owner should be kind of unique. This will be used to remove the
* listener.
*/
on(eventType: string, owner: any, callback: Callback) {
if (!callback) {
throw new Error("Missing callback");
}
if (!this.subscriptions[eventType]) {
this.subscriptions[eventType] = [];
}
this.subscriptions[eventType].push({
owner,
callback
});
}
/**
* Remove a listener
*/
off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType];
if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
}
}
/**
* Emit an event of type 'eventType'. Any extra arguments will be passed to
* the listeners callback.
*/
trigger(eventType: string, ...args: any[]) {
const subs = this.subscriptions[eventType] || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
sub.callback.call(sub.owner, ...args);
}
}
/**
* Remove all subscriptions.
*/
clear() {
this.subscriptions = {};
}
}
+109
View File
@@ -0,0 +1,109 @@
/**
* Owl Observer
*
* This code contains the logic that allows Owl to observe and react to state
* changes.
*
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
* - Objects and arrays are observed by replacing them with a Proxy
* - each object/array metadata are tracked in a weakmap, and keep a revision
* number
*
* Note that this code is loosely inspired by Vue.
*/
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
return value;
}
let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy;
}
revNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0;
}
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) {
var self = this;
const proxy = new Proxy(value, {
get(target, k) {
const targetValue = target[k];
return self.observe(targetValue, value);
},
set(target, key: string, newVal): boolean {
const value = target[key];
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
}
return true;
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
}
return true;
}
});
const metadata = {
value,
proxy,
rev: this.rev,
deepRev: this.rev,
parent
};
this.weakMap.set(value, metadata);
this.weakMap.set(metadata.proxy, metadata);
return metadata;
}
_updateRevNumber(target: any) {
this.rev++;
let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.deepRev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
+104
View File
@@ -0,0 +1,104 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
// -----------------------------------------------------------------------------
// useState
// -----------------------------------------------------------------------------
/**
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component._current;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
// -----------------------------------------------------------------------------
// Life cycle hooks
// -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) {
return function(cb) {
const component: Component<any, any> = Component._current;
if (component.__owl__[method]) {
const current = component.__owl__[method];
if (reverse) {
component.__owl__[method] = function() {
current.call(component);
cb.call(component);
};
} else {
component.__owl__[method] = function() {
cb.call(component);
current.call(component);
};
}
} else {
component.__owl__[method] = cb;
}
};
}
export const onMounted = makeLifecycleHook("mountedCB", true);
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
export const onWillPatch = makeLifecycleHook("willPatchCB");
export const onPatched = makeLifecycleHook("patchedCB", true);
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
}
export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
/**
* 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(nextEnv) {
const component = Component._current;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
+44 -14
View File
@@ -1,16 +1,46 @@
import { TemplateSet } from "./runtime/template_set";
import { compile } from "./compiler";
/**
* This file is the main file packaged by rollup (see rollup.config.js). From
* this file, we export all public owl elements.
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index";
import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import * as _hooks from "./hooks";
import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router";
export * from "./runtime";
export { Component } from "./component/component";
export { QWeb };
TemplateSet.prototype._compileTemplate = function _compileTemplate(
name: string,
template: string | Element
) {
return compile(template, {
name,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
};
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent };
export const utils = _utils;
export const tags = _tags;
export const hooks = _hooks;
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+360
View File
@@ -0,0 +1,360 @@
import { Context } from "./context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
/**
* Owl QWeb Directives
*
* This file contains the implementation of most standard QWeb directives:
* - t-esc
* - t-raw
* - t-set/t-value
* - t-if/t-elif/t-else
* - t-call
* - t-foreach/t-as
* - t-debug
* - t-log
*/
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
if (value === "0" && ctx.caller) {
qweb._compileNode(ctx.caller, ctx);
return;
}
if (value.xml instanceof NodeList) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`result = vn${nodeID}`);
}
} else {
let fragID = ctx.generateID();
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
}
QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
}
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
}
});
QWeb.addDirective({
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
}
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
}
});
//------------------------------------------------------------------------------
// t-set
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
}
return true;
}
});
//------------------------------------------------------------------------------
// t-if, t-elif, t-else
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "if",
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(`${ctx.formatExpression(cond)}`);
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "elif",
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "else",
priority: 40,
atNodeEncounter({ ctx }): boolean {
ctx.addLine(`else {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
//------------------------------------------------------------------------------
// t-call
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!;
const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new Context();
tempCtx.nextID = ctx.rootContext.nextID;
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
ctx.rootContext.nextID = tempCtx.nextID;
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
ctx.indent();
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
}
// todo: handle XML variables...
}
}
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
ctx.dedent();
ctx.addLine("}");
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
//------------------------------------------------------------------------------
// t-foreach
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "foreach",
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx = ctx.subContext("inLoop", true);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addToScope(name + "_first", "i === 0");
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
ctx.addToScope(name + "_index", "i");
ctx.addToScope(name, `_${keysID}[i]`);
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (
!shouldWarn &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key")
) {
shouldWarn = true;
}
}
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
return true;
}
});
//------------------------------------------------------------------------------
// t-debug
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
});
//------------------------------------------------------------------------------
// t-log
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
}
});
+179
View File
@@ -0,0 +1,179 @@
import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
lastNodeKey: string = ""; // temp variable to communicate to previous caller
templates: { [key: string]: boolean } = {};
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
}
withParent(node: number): Context {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
if (!this.parentNode) {
this.addLine(`result = vn${node};`);
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
return compileExpr(expr, this.variables);
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
+264
View File
@@ -0,0 +1,264 @@
/**
* Owl QWeb Expression Parser
*
* Owl needs in various contexts to be able to understand the structure of a
* string representing a javascript expression. The usual goal is to be able
* to rewrite some variables. For example, if a template has
*
* ```xml
* <t t-if="computeSomething({val: state.val})">...</t>
* ```
*
* this needs to be translated in something like this:
*
* ```js
* if (context["computeSomething"]({val: context["state"].val})) { ... }
* ```
*
* This file contains the implementation of an extremely naive tokenizer/parser
* and evaluator for javascript expressions. The supported grammar is basically
* only expressive enough to understand the shape of objects, of arrays, and
* various operators.
*/
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
type TKind =
| "LEFT_BRACE"
| "RIGHT_BRACE"
| "LEFT_BRACKET"
| "RIGHT_BRACKET"
| "LEFT_PAREN"
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "SYMBOL"
| "OPERATOR"
| "COLON";
interface Token {
type: TKind;
value: string;
size?: number;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
"]": "RIGHT_BRACKET",
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
return false;
}
let i = 1;
let cur;
while (expr[i] && expr[i] !== start) {
cur = expr[i];
s += cur;
if (cur === "\\") {
i++;
cur = expr[i];
if (!cur) {
throw new Error("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new Error("Invalid expression");
}
s += start;
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
let i = 1;
while (expr[i] && expr[i].match(/[0-9]|\./)) {
s += expr[i];
i++;
}
return { type: "VALUE", value: s };
} else {
return false;
}
};
let tokenizeSymbol: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1;
while (expr[i] && expr[i].match(/\w/)) {
s += expr[i];
i++;
}
if (s in WORD_REPLACEMENT) {
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
}
return { type: "SYMBOL", value: s };
} else {
return false;
}
};
const tokenizeStatic: Tokenizer = function(expr) {
const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
}
return false;
};
const tokenizeOperator: Tokenizer = function(expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
}
}
return false;
};
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
* example: `tokenize("1 + b")` will return:
* ```js
* [
* {type: "VALUE", value: "1"},
* {type: "OPERATOR", value: "+"},
* {type: "SYMBOL", value: "b"}
* ]
* ```
*/
export function tokenize(expr: string): Token[] {
const result: Token[] = [];
let token: boolean | Token = true;
while (token) {
expr = expr.trim();
if (expr) {
for (let tokenizer of TOKENIZERS) {
token = tokenizer(expr);
if (token) {
result.push(token);
expr = expr.slice(token.size || token.value.length);
break;
}
}
} else {
token = false;
}
}
if (expr.length) {
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
}
return result;
}
//------------------------------------------------------------------------------
// Expression "evaluator"
//------------------------------------------------------------------------------
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
* proper lookups in the context.
*
* Usually, this kind of code would be very simple to do if we had an AST (so,
* if we had a javascript parser), since then, we would only need to find the
* variables and replace them. However, a parser is more complicated, and there
* are no standard builtin parser API.
*
* Since this method is applied to simple javasript expressions, and the work to
* be done is actually quite simple, we actually can get away with not using a
* parser, which helps with the code size.
*
* Here is the heuristic used by this method to determine if a token is a
* variable:
* - by default, all symbols are considered a variable
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
} else {
token.value = `context['${token.value}']`;
}
}
}
result += token.value;
}
return result;
}
+251
View File
@@ -0,0 +1,251 @@
import { VNode } from "../vdom/index";
import { QWeb } from "./qweb";
/**
* Owl QWeb Extensions
*
* This file contains the implementation of non standard QWeb directives, added
* by Owl and that will only work on Owl projects:
*
* - t-on
* - t-ref
* - t-transition
* - t-mounted
* - t-slot
* - t-model
*/
//------------------------------------------------------------------------------
// t-on
//------------------------------------------------------------------------------
// these are pieces of code that will be injected into the event handler if
// modifiers are specified
export const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
};
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
whenTransitionEnd(elm, finalize);
});
};
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
whenTransitionEnd(elm, finalize);
});
};
function getTimeout(delays: Array<string>, durations: Array<string>): number {
/* istanbul ignore next */
while (delays.length < durations.length) {
delays = delays.concat(delays);
}
return Math.max.apply(
null,
durations.map((d, i) => {
return toMs(d) + toMs(delays[i]);
})
);
}
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
function toMs(s: string): number {
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
}
function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
);
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
event = "change";
handler = `(ev) => {${expr} = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
handler = `(ev) => {${expr} = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
handler = `(ev) => {${expr} = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
+4
View File
@@ -0,0 +1,4 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
+733
View File
@@ -0,0 +1,733 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
/**
* Owl QWeb Engine
*
* In this file, you will find a QWeb engine/template compiler. It is the core
* of how Owl component works.
*
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
* DOM representation.
*
* We have here:
* - a CompilationContext class, which is an internal object that contains all
* compilation specific information, while a template is being compiled.
* - a QWeb class: this is the code of the QWeb compiler.
*
* Note that this file does not contain the implementation of the QWeb
* directives (see qweb_directives.ts and qweb_extensions.ts).
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type EvalContext = { [key: string]: any };
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
interface Template {
elem: Element;
fn: CompiledTemplate;
}
interface CompilationInfo {
node: Element;
qweb: QWeb;
ctx: Context;
fullName: string;
value: string;
}
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive {
name: string;
extraNames?: string[];
priority: number;
// if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void;
}
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)"
};
interface Utils {
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
const UTILS: Utils = {
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return expr;
},
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
}
};
function parseXML(xml: string): Document {
const parser = new DOMParser();
// we remove comments from the xml string
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
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;
}
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template };
static utils = UTILS;
static components = Object.create(null);
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
name: 1,
att: 1,
attf: 1,
key: 1
};
static DIRECTIVES: Directive[] = [];
static TEMPLATES: { [name: string]: Template } = {};
static nextId: number = 1;
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
static slots = {};
static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
isUpdating: boolean = false;
constructor(data?: string) {
super();
this.templates = Object.create(QWeb.TEMPLATES);
if (data) {
this.addTemplates(data);
}
}
static addDirective(directive: Directive) {
if (directive.name in QWeb.DIRECTIVE_NAMES) {
throw new Error(`Directive "${directive.name} already registered`);
}
QWeb.DIRECTIVES.push(directive);
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
static registerComponent(name: string, Component: any) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.components[name] = Component;
}
/**
* Register globally a template. All QWeb instances will obtain their
* templates from their own template map, and then, from the global static
* TEMPLATES property.
*/
static registerTemplate(name: string, template: string) {
if (QWeb.TEMPLATES[name]) {
throw new Error(`Template '${name}' has already been registered`);
}
const qweb = new QWeb();
qweb.addTemplate(name, template);
QWeb.TEMPLATES[name] = qweb.templates[name];
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
}
this._addTemplate(name, <Element>doc.firstChild);
}
/**
* Load templates from a xml (as a string or xml document). This will look up
* for the first <templates> tag, and will consider each child of this as a
* template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr: string | Document) {
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
}
for (let elem of <any>templates.children) {
const name = elem.getAttribute("t-name");
this._addTemplate(name, elem);
}
}
_addTemplate(name: string, elem: Element) {
if (name in this.templates) {
throw new Error(`Template ${name} already defined`);
}
this._processTemplate(elem);
const template = {
elem,
fn: function(this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
}
};
this.templates[name] = template;
}
_processTemplate(elem: Element) {
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i];
let prevElem = node.previousElementSibling!;
let pattr = function(name) {
return prevElem.getAttribute(name);
};
let nattr = function(name) {
return +!!node.getAttribute(name);
};
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) {
throw new Error(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
);
}
if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
return a + b;
}) > 1
) {
throw new Error("Only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode.nodeValue.trim().length) {
throw new Error("text is not allowed between branching directives");
}
textNode.remove();
}
} else {
throw new Error(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
);
}
}
}
/**
* Render a template
*
* @param {string} name the template should already have been added
*/
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
const template = this.templates[name];
if (!template) {
throw new Error(`Template ${name} does not exist`);
}
return template.fn.call(this, context, extra);
}
/**
* Render a template to a html string.
*
* Note that this is more limited than the `render` method: it is not suitable
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
}
/**
* Force all widgets connected to this QWeb instance to rerender themselves.
*
* This method is mostly useful for external code that want to modify the
* application in some cases. For example, a router plugin.
*/
forceUpdate() {
this.isUpdating = true;
Promise.resolve().then(() => {
if (this.isUpdating) {
this.isUpdating = false;
this.trigger("update");
}
});
}
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
if (!parentContext) {
if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`);
} else {
if (!ctx.rootNode) {
throw new Error(`A template should have one root node (${ctx.templateName})`);
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: Context) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
let withHandlers = false;
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
}
}
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
for (let i = 0; i < DIR_N; i++) {
let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value
});
if (isDone) {
return;
}
}
}
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
let nodeHooks = {};
let addNodeHook = function(hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
directive.atNodeCreation({
node,
qweb: this,
ctx,
fullName,
value,
nodeID,
addNodeHook
});
}
}
if (Object.keys(nodeHooks).length) {
ctx.addLine(`p${nodeID}.hook = {`);
for (let hook in nodeHooks) {
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
for (let handler of nodeHooks[hook]) {
ctx.addLine(` ${handler}`);
}
ctx.addLine(` },`);
}
ctx.addLine(`};`);
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS =
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value });
}
}
}
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
const formattedExpr = ctx.interpolate(value);
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`var _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
nodeKey = `nodeKey${nodeID}`;
ctx.lastNodeKey = nodeKey;
} else {
nodeKey = nodeID;
}
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
ctx.addElse();
ctx.addLine(`for (let key in _${id}) {`);
ctx.indent();
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
ctx.dedent();
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: Context) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./Router";
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>
`;
href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) {
this.href = this.env.router.destToPath(nextProps);
}
get isActive() {
if (this.env.router.mode === "hash") {
return (<any>document.location).hash === this.href;
}
return (<any>document.location).pathname === this.href;
}
navigate(ev) {
// don't redirect with control keys
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
// don't redirect on right click
if (ev.button !== undefined && ev.button !== 0) {
return;
}
// don't redirect if `target="_blank"`
if (ev.currentTarget && ev.currentTarget.getAttribute) {
const target = ev.currentTarget.getAttribute("target");
if (/\b_blank\b/i.test(target)) {
return;
}
}
ev.preventDefault();
this.env.router.navigate(this.props);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Component } from "../component/component";
import { xml } from "../tags";
export class RouteComponent extends Component<any, {}> {
static template = xml`
<t>
<t
t-if="routeComponent"
t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t>
`;
get routeComponent(): any {
return this.env.router.currentRoute && this.env.router.currentRoute.component;
}
}
+275
View File
@@ -0,0 +1,275 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { shallowEqual } from "../utils";
type NavigationGuard = (info: {
env: Env;
to: Route | null;
from: Route | null;
}) => boolean | Destination;
export interface Route {
name: string;
path: string;
component?: any;
redirect?: Destination;
params: string[];
beforeRouteEnter?: NavigationGuard;
}
export type RouteParams = { [key: string]: string | number };
export interface RouterEnv extends Env {
router: Router;
}
export interface Destination {
path?: string;
to?: string;
params?: RouteParams;
}
interface PositiveMatchResult {
type: "match";
route: Route;
params: RouteParams;
}
interface NegativeMatchResult {
type: "nomatch";
}
interface CancelledMatch {
type: "cancelled";
}
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
interface Options {
mode: Router["mode"];
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
currentRoute: Route | null = null;
currentParams: RouteParams | null = null;
mode: "history" | "hash";
routes: { [id: string]: Route };
routeIds: string[];
env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
this.routes = {};
this.routeIds = [];
let nextId = 1;
for (let partialRoute of routes) {
if (!partialRoute.name) {
partialRoute.name = "__route__" + nextId++;
}
if (partialRoute.component) {
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
}
if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
(this as any)._listener = ev => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener);
}
const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") {
this.currentRoute = result.route;
this.currentParams = result.params;
const currentPath = this.routeToPath(result.route, result.params);
if (currentPath !== this.currentPath()) {
this.setUrlFromPath(currentPath);
}
}
}
async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to);
return this._navigate(path);
}
async _navigate(path: string, ev?: any): Promise<boolean> {
const initialName = this.currentRouteName;
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath);
}
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
const didChange =
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
if (didChange) {
this.env.qweb.forceUpdate();
return true;
}
return false;
}
destToPath(dest: Destination): string {
this.validateDestination(dest);
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
}
get currentRouteName(): string | null {
return this.currentRoute && this.currentRoute.name;
}
//--------------------------------------------------------------------------
// Private helpers
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const separator = this.mode === "hash" ? "/" : "";
const url = location.origin + separator + path;
if (url !== window.location.href) {
window.history.pushState({}, path, url);
}
}
private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
}
}
private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
const prefix = this.mode === "hash" ? "#" : "";
return prefix + parts.join("/");
}
private currentPath(): string {
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
return result || "/";
}
private match(path: string): MatchResult {
for (let routeId of this.routeIds) {
let route = this.routes[routeId];
let params = this.getRouteParams(route, path);
if (params) {
return {
type: "match",
route: route,
params: params
};
}
}
return { type: "nomatch" };
}
private async matchAndApplyRules(path: string): Promise<MatchResult> {
const result = this.match(path);
if (result.type === "match") {
return this.applyRules(result);
}
return result;
}
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
const route = matchResult.route;
if (route.redirect) {
const path = this.destToPath(route.redirect);
return this.matchAndApplyRules(path);
}
if (route.beforeRouteEnter) {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route
});
if (result === false) {
return { type: "cancelled" };
} else if (result !== true) {
// we want to navigate to another destination
const path = this.destToPath(result);
return this.matchAndApplyRules(path);
}
}
return matchResult;
}
private getRouteParams(route: Route, path: string): RouteParams | false {
if (route.path === "*") {
return {};
}
if (path.startsWith("#")) {
path = path.slice(1);
}
const descrParts = route.path.split("/");
const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false;
}
const result = {};
for (let i = 0; i < l; i++) {
const descr = descrParts[i];
let target: string | number = targetParts[i];
const match = descr.match(paramRegexp);
if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
}
}
return result;
}
}
function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1].split(".")[0]);
}
} while (m);
return result;
}
-187
View File
@@ -1,187 +0,0 @@
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers, OwlError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils";
import { handleError } from "./error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export interface Env {
[key: string]: any;
}
export interface AppConfig<P, E> extends TemplateSetConfig {
props?: P;
env?: E;
test?: boolean;
warnIfNoStaticProps?: boolean;
}
let hasBeenLogged = false;
export const DEV_MSG = () => {
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
return `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
export class App<
T extends abstract new (...args: any) => any = any,
P extends object = any,
E = any
> extends TemplateSet {
static validateTarget = validateTarget;
Root: ComponentConstructor<P, E>;
props: P;
env: E;
scheduler = new Scheduler();
root: ComponentNode<P, E> | null = null;
warnIfNoStaticProps: boolean;
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config);
this.Root = Root;
if (config.test) {
this.dev = true;
}
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
if (this.dev && !config.test && !hasBeenLogged) {
console.info(DEV_MSG());
hasBeenLogged = true;
}
const env = config.env || {};
const descrs = Object.getOwnPropertyDescriptors(env);
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
this.props = config.props || ({} as P);
}
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
if (this.dev) {
validateProps(this.Root, this.props, { app: this });
}
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
}
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this, null, null);
}
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) {
reject(e);
}
throw e;
});
});
node.mountComponent(target, options);
return promise;
}
destroy() {
if (this.root) {
this.scheduler.flush();
this.root.destroy();
}
}
createComponent<P extends Props>(
name: string | null,
isStatic: boolean,
hasSlotsProp: boolean,
hasDynamicPropList: boolean,
hasNoProp: boolean
) {
const isDynamic = !isStatic;
function _arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
}
const arePropsDifferent = hasSlotsProp
? (_1: any, _2: any) => true
: hasNoProp
? (_1: any, _2: any) => false
: _arePropsDifferent;
const updateAndRender = ComponentNode.prototype.updateAndRender;
const initiateRender = ComponentNode.prototype.initiateRender;
return (props: P, key: string, ctx: ComponentNode, parent: any, C: any) => {
let children = ctx.children;
let node: any = children[key];
if (isDynamic && node && node.component.constructor !== C) {
node = undefined;
}
const parentFiber = ctx.fiber!;
if (node) {
if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
node.forceNextRender = false;
updateAndRender.call(node, props, parentFiber);
}
} else {
// new component
if (isStatic) {
C = parent.constructor.components[name as any];
if (!C) {
throw new OwlError(`Cannot find the definition of component "${name}"`);
} else if (!(C.prototype instanceof Component)) {
throw new OwlError(
`"${name}" is not a Component. It must inherit from the Component class`
);
}
}
node = new ComponentNode(C, props, this, ctx, key);
children[key] = node;
initiateRender.call(node, new Fiber(node, parentFiber));
}
parentFiber.childrenMap[key] = node;
return node;
};
}
handleError(...args: Parameters<typeof handleError>) {
return handleError(...args);
}
}
export async function mount<
T extends abstract new (...args: any) => any = any,
P extends object = any,
E = any
>(
C: T & ComponentConstructor<P, E>,
target: HTMLElement,
config: AppConfig<P, E> & MountOptions = {}
): Promise<Component<P, E> & InstanceType<T>> {
return new App(C, config).mount(target, config);
}
-172
View File
@@ -1,172 +0,0 @@
import type { Setter } from "./block_compiler";
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
const tokenList = DOMTokenList.prototype;
const tokenListAdd = tokenList.add;
const tokenListRemove = tokenList.remove;
const isArray = Array.isArray;
const { split, trim } = String.prototype;
const wordRegexp = /\s+/;
/**
* We regroup here all code related to updating attributes in a very loose sense:
* attributes, properties and classs are all managed by the functions in this
* 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> {
return function (this: HTMLElement, value: any) {
setAttribute.call(this, attr, value);
};
}
export function attrsSetter(this: HTMLElement, attrs: any) {
if (isArray(attrs)) {
setAttribute.call(this, attrs[0], attrs[1]);
} else {
for (let k in attrs) {
setAttribute.call(this, k, attrs[k]);
}
}
}
export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
if (isArray(attrs)) {
const name = attrs[0];
const val = attrs[1];
if (name === oldAttrs[0]) {
if (val === oldAttrs[1]) {
return;
}
setAttribute.call(this, name, val);
} else {
removeAttribute.call(this, oldAttrs[0]);
setAttribute.call(this, name, val);
}
} else {
for (let k in oldAttrs) {
if (!(k in attrs)) {
removeAttribute.call(this, k);
}
}
for (let k in attrs) {
const val = attrs[k];
if (val !== oldAttrs[k]) {
setAttribute.call(this, k, val);
}
}
}
}
function toClassObj(expr: string | number | { [c: string]: any }) {
const result: { [c: string]: any } = {};
switch (typeof expr) {
case "string":
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
const str = trim.call(expr);
if (!str) {
return {};
}
let words = split.call(str, wordRegexp);
for (let i = 0, l = words.length; i < l; i++) {
result[words[i]] = true;
}
return result;
case "object":
// this is already an object but we may need to split keys:
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
for (let key in expr as any) {
const value = (expr as any)[key];
if (value) {
key = trim.call(key);
if (!key) {
continue;
}
const words = split.call(key, wordRegexp);
for (let word of words) {
result[word] = value;
}
}
}
return result;
case "undefined":
return {};
case "number":
return { [expr as number]: true };
default:
return { [expr as any]: true };
}
}
export function setClass(this: HTMLElement, val: any) {
val = val === "" ? {} : toClassObj(val);
// add classes
const cl = this.classList;
for (let c in val) {
tokenListAdd.call(cl, c);
}
}
export function updateClass(this: HTMLElement, val: any, oldVal: any) {
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
val = val === "" ? {} : toClassObj(val);
const cl = this.classList;
// remove classes
for (let c in oldVal) {
if (!(c in val)) {
tokenListRemove.call(cl, c);
}
}
// add classes
for (let c in val) {
if (!(c in oldVal)) {
tokenListAdd.call(cl, c);
}
}
}
export function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) {
// support 0, fallback to empty string for other falsy values
(this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
};
}
export function isProp(tag: string, key: string): boolean {
switch (tag) {
case "input":
return (
key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "disabled"
);
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
-644
View File
@@ -1,644 +0,0 @@
import { OwlError } from "../error_handling";
import {
attrsSetter,
attrsUpdater,
createAttrUpdater,
isProp,
makePropSetter,
setClass,
updateClass,
} from "./attributes";
import { config } from "./config";
import { createEventHandler } from "./events";
import type { VNode } from "./index";
import { VMulti } from "./multi";
import { toText } from "./text";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const elementProto = Element.prototype;
const characterDataProto = CharacterData.prototype;
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeGetFirstChild = getDescriptor(nodeProto, "firstChild").get!;
const nodeGetNextSibling = getDescriptor(nodeProto, "nextSibling").get!;
const NO_OP = () => {};
// -----------------------------------------------------------------------------
// Main compiler code
// -----------------------------------------------------------------------------
type BlockType = (data?: any[], children?: VNode[]) => VNode;
const cache: { [key: string]: BlockType } = {};
/**
* Compiling blocks is a multi-step process:
*
* 1. build an IntermediateTree from the HTML element. This intermediate tree
* is a binary tree structure that encode dynamic info sub nodes, and the
* path required to reach them
* 2. process the tree to build a block context, which is an object that aggregate
* all dynamic info in a list, and also, all ref indexes.
* 3. process the context to build appropriate builder/setter functions
* 4. make a dynamic block class, which will efficiently collect references and
* create/update dynamic locations/children
*
* @param str
* @returns a new block type, that can build concrete blocks
*/
export function createBlock(str: string): BlockType {
if (str in cache) {
return cache[str];
}
// step 0: prepare html base element
const doc = new DOMParser().parseFromString(`<t>${str}</t>`, "text/xml");
const node = doc.firstChild!.firstChild!;
if (config.shouldNormalizeDom) {
normalizeNode(node as any);
}
// step 1: prepare intermediate tree
const tree = buildTree(node);
// step 2: prepare block context
const context = buildContext(tree);
// step 3: build the final block class
const template = tree.el as HTMLElement;
const Block = buildBlock(template, context);
cache[str] = Block;
return Block;
}
// -----------------------------------------------------------------------------
// Helper
// -----------------------------------------------------------------------------
function normalizeNode(node: HTMLElement | Text) {
if (node.nodeType === Node.TEXT_NODE) {
if (!/\S/.test((node as Text).textContent!)) {
(node as Text).remove();
return;
}
}
if (node.nodeType === Node.ELEMENT_NODE) {
if ((node as HTMLElement).tagName === "pre") {
return;
}
}
for (let i = node.childNodes.length - 1; i >= 0; --i) {
normalizeNode(node.childNodes.item(i) as any);
}
}
// -----------------------------------------------------------------------------
// building a intermediate tree
// -----------------------------------------------------------------------------
interface DynamicInfo {
idx: number;
refIdx?: number;
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
isOnlyChild?: boolean;
name?: string;
tag?: string;
event?: string;
}
interface IntermediateTree {
parent: IntermediateTree | null;
firstChild: IntermediateTree | null;
nextSibling: IntermediateTree | null;
el: Node;
info: DynamicInfo[];
isRef?: boolean;
refIdx?: number;
refN: number;
currentNS: string | null;
}
function buildTree(
node: Node,
parent: IntermediateTree | null = null,
domParentTree: IntermediateTree | null = null
): IntermediateTree {
switch (node.nodeType) {
case Node.ELEMENT_NODE: {
// HTMLElement
let currentNS = domParentTree && domParentTree.currentNS;
const tagName = (node as Element).tagName;
let el: Node | undefined = undefined;
const info: DynamicInfo[] = [];
if (tagName.startsWith("block-text-")) {
const index = parseInt(tagName.slice(11), 10);
info.push({ type: "text", idx: index });
el = document.createTextNode("");
}
if (tagName.startsWith("block-child-")) {
if (!domParentTree!.isRef) {
addRef(domParentTree!);
}
const index = parseInt(tagName.slice(12), 10);
info.push({ type: "child", idx: index });
el = document.createTextNode("");
}
const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
}
if (!el) {
el = currentNS
? document.createElementNS(currentNS, tagName)
: document.createElement(tagName);
}
if (el instanceof Element) {
if (!domParentTree) {
// some html elements may have side effects when setting their attributes.
// For example, setting the src attribute of an <img/> will trigger a
// request to get the corresponding image. This is something that we
// don't want at compile time. We avoid that by putting the content of
// the block in a <template/> element
const fragment = document.createElement("template").content;
fragment.appendChild(el);
}
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
const attrValue = attrs[i].value;
if (attrName.startsWith("block-handler-")) {
const idx = parseInt(attrName.slice(14), 10);
info.push({
type: "handler",
idx,
event: attrValue,
});
} else if (attrName.startsWith("block-attribute-")) {
const idx = parseInt(attrName.slice(16), 10);
info.push({
type: "attribute",
idx,
name: attrValue,
tag: tagName,
});
} else if (attrName === "block-attributes") {
info.push({
type: "attributes",
idx: parseInt(attrValue, 10),
});
} else if (attrName === "block-ref") {
info.push({
type: "ref",
idx: parseInt(attrValue, 10),
});
} else {
el.setAttribute(attrs[i].name, attrValue);
}
}
}
const tree: IntermediateTree = {
parent,
firstChild: null,
nextSibling: null,
el,
info,
refN: 0,
currentNS,
};
if (node.firstChild) {
const childNode = node.childNodes[0];
if (
node.childNodes.length === 1 &&
childNode.nodeType === Node.ELEMENT_NODE &&
(childNode as Element).tagName.startsWith("block-child-")
) {
const tagName = (childNode as Element).tagName;
const index = parseInt(tagName.slice(12), 10);
info.push({ idx: index, type: "child", isOnlyChild: true });
} else {
tree.firstChild = buildTree(node.firstChild, tree, tree);
el.appendChild(tree.firstChild.el);
let curNode: Node | null = node.firstChild;
let curTree: IntermediateTree | null = tree.firstChild;
while ((curNode = curNode.nextSibling)) {
curTree.nextSibling = buildTree(curNode, curTree, tree);
el.appendChild(curTree.nextSibling.el);
curTree = curTree.nextSibling;
}
}
}
if (tree.info.length) {
addRef(tree);
}
return tree;
}
case Node.TEXT_NODE:
case Node.COMMENT_NODE: {
// text node or comment node
const el =
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!);
return {
parent: parent,
firstChild: null,
nextSibling: null,
el,
info: [],
refN: 0,
currentNS: null,
};
}
}
throw new OwlError("boom");
}
function addRef(tree: IntermediateTree) {
tree.isRef = true;
do {
tree.refN++;
} while ((tree = tree.parent as any));
}
function parentTree(tree: IntermediateTree): IntermediateTree | null {
let parent = tree.parent;
while (parent && parent.nextSibling === tree) {
tree = parent;
parent = parent.parent;
}
return parent;
}
// -----------------------------------------------------------------------------
// Building a block context
// -----------------------------------------------------------------------------
interface RefCollector {
idx: number;
prevIdx: number;
getVal: Function;
}
export type Setter<T = any> = (this: T, value: any) => void;
export type Updater<T = any> = (this: T, value: any, oldVal: any) => void;
interface Location {
refIdx: number;
setData: Setter;
updateData: Updater;
}
interface IndexedLocation extends Location {
idx: number;
}
interface Child {
parentRefIdx: number;
afterRefIdx?: number;
isOnlyChild?: boolean;
}
interface BlockCtx {
refN: number;
collectors: RefCollector[];
locations: IndexedLocation[];
children: Child[];
cbRefs: number[];
refList: (() => void)[][];
}
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
if (!ctx) {
const children = new Array(tree.info.filter((v) => v.type === "child").length);
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };
fromIdx = 0;
}
if (tree.refN) {
const initialIdx = fromIdx!;
const isRef = tree.isRef;
const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
//node
if (isRef) {
for (let info of tree.info) {
info.refIdx = initialIdx!;
}
tree.refIdx = initialIdx!;
updateCtx(ctx, tree);
fromIdx!++;
}
// right
if (nextSibling) {
const idx = fromIdx! + firstChild;
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
buildContext(tree.nextSibling!, ctx, idx);
}
// left
if (firstChild) {
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
buildContext(tree.firstChild!, ctx, fromIdx!);
}
}
return ctx;
}
function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
for (let info of tree.info) {
switch (info.type) {
case "text":
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setText,
updateData: setText,
});
break;
case "child":
if (info.isOnlyChild) {
// tree is the parentnode here
ctx.children[info.idx] = {
parentRefIdx: info.refIdx!,
isOnlyChild: true,
};
} else {
// tree is the anchor text node
ctx.children[info.idx] = {
parentRefIdx: parentTree(tree)!.refIdx!,
afterRefIdx: info.refIdx!,
};
}
break;
case "attribute": {
const refIdx = info.refIdx!;
let updater: any;
let setter: any;
if (isProp(info.tag!, info.name!)) {
const setProp = makePropSetter(info.name!);
setter = setProp;
updater = setProp;
} else if (info.name === "class") {
setter = setClass;
updater = updateClass;
} else {
setter = createAttrUpdater(info.name!);
updater = setter;
}
ctx.locations.push({
idx: info.idx,
refIdx,
setData: setter,
updateData: updater,
});
break;
}
case "attributes":
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: attrsSetter,
updateData: attrsUpdater,
});
break;
case "handler": {
const { setup, update } = createEventHandler(info.event!);
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setup,
updateData: update,
});
break;
}
case "ref":
const index = ctx.cbRefs.push(info.idx) - 1;
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: makeRefSetter(index, ctx.refList),
updateData: NO_OP,
});
}
}
}
// -----------------------------------------------------------------------------
// building the concrete block class
// -----------------------------------------------------------------------------
function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
let B = createBlockClass(template, ctx);
if (ctx.cbRefs.length) {
const cbRefs = ctx.cbRefs;
const refList = ctx.refList;
let cbRefsNumber = cbRefs.length;
B = class extends B {
mount(parent: HTMLElement, afterNode: Node | null) {
refList.push(new Array(cbRefsNumber));
super.mount(parent, afterNode);
for (let cbRef of refList.pop()!) {
cbRef();
}
}
remove() {
super.remove();
for (let cbRef of cbRefs) {
let fn = (this as any).data[cbRef];
fn(null);
}
}
};
}
if (ctx.children.length) {
B = class extends B {
children: (VNode | undefined)[] | undefined;
constructor(data?: any[], children?: VNode[]) {
super(data);
this.children = children;
}
};
B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children);
}
return (data?: any[]) => new B(data);
}
type Constructor<T> = new (...args: any[]) => T;
type BlockClass = Constructor<VNode<any>>;
function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
const { refN, collectors, children } = ctx;
const colN = collectors.length;
ctx.locations.sort((a, b) => a.idx - b.idx);
const locations: Location[] = ctx.locations.map((loc) => ({
refIdx: loc.refIdx,
setData: loc.setData,
updateData: loc.updateData,
}));
const locN = locations.length;
const childN = children.length;
const childrenLocs = children;
const isDynamic = refN > 0;
// these values are defined here to make them faster to lookup in the class
// block scope
const nodeCloneNode = nodeProto.cloneNode;
const nodeInsertBefore = nodeProto.insertBefore;
const elementRemove = elementProto.remove;
class Block {
el: HTMLElement | undefined;
parentEl?: HTMLElement | undefined;
data: any[] | undefined;
children?: (VNode | undefined)[];
refs: Node[] | undefined;
constructor(data?: any[]) {
this.data = data;
}
beforeRemove() {}
remove() {
elementRemove.call(this.el);
}
firstNode(): Node {
return this.el!;
}
*nodes() {
if (this.el) {
yield this.el;
}
}
moveBefore(other: Block | null, afterNode: Node | null) {
const target = other ? other.el! : afterNode;
nodeInsertBefore.call(this.parentEl, this.el!, target);
}
toString() {
const div = document.createElement("div");
this.mount(div, null);
return div.innerHTML;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true) as HTMLElement;
nodeInsertBefore.call(parent, el, afterNode);
this.el = el;
this.parentEl = parent;
}
patch(other: Block, withBeforeRemove: boolean) {}
}
if (isDynamic) {
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true);
// collecting references
const refs: Node[] = new Array(refN);
this.refs = refs;
refs[0] = el;
for (let i = 0; i < colN; i++) {
const w = collectors[i];
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
}
// applying data to all update points
if (locN) {
const data = this.data!;
for (let i = 0; i < locN; i++) {
const loc = locations[i];
loc.setData.call(refs[loc.refIdx], data[i]);
}
}
nodeInsertBefore.call(parent, el, afterNode);
// preparing all children
if (childN) {
const children = this.children;
for (let i = 0; i < childN; i++) {
const child = children![i];
if (child) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child.isOnlyChild = loc.isOnlyChild;
child.mount(refs[loc.parentRefIdx] as any, afterNode);
}
}
}
this.el = el as HTMLElement;
this.parentEl = parent;
};
Block.prototype.patch = function patch(other: Block, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const refs = this.refs!;
// update texts/attributes/
if (locN) {
const data1 = this.data!;
const data2 = other.data!;
for (let i = 0; i < locN; i++) {
const val1 = data1[i];
const val2 = data2[i];
if (val1 !== val2) {
const loc = locations[i];
loc.updateData.call(refs[loc.refIdx], val2, val1);
}
}
this.data = data2;
}
// update children
if (childN) {
let children1 = this.children;
const children2 = other.children;
for (let i = 0; i < childN; i++) {
const child1 = children1![i];
const child2 = children2![i];
if (child1) {
if (child2) {
child1.patch(child2, withBeforeRemove);
} else {
if (withBeforeRemove) {
child1.beforeRemove();
}
child1.remove();
children1![i] = undefined;
}
} else if (child2) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child2.mount(refs[loc.parentRefIdx] as any, afterNode);
children1![i] = child2;
}
}
}
};
}
return Block;
}
function setText(this: Text, value: any) {
characterDataSetData.call(this, toText(value));
}
function makeRefSetter(index: number, refs: (() => void)[][]): Setter<HTMLElement> {
return function setRef(this: HTMLElement, fn: any) {
refs[refs.length - 1][index] = () => fn(this);
};
}
-28
View File
@@ -1,28 +0,0 @@
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 = {
// whether or not blockdom should normalize DOM whenever a block is created.
// Normalizing dom mean removing empty text nodes (or containing only spaces)
shouldNormalizeDom: true,
// 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
// and the event
mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => {
if (typeof data === "function") {
data(ev);
} else if (Array.isArray(data)) {
data = filterOutModifiersFromData(data).data;
data[0](data[1], ev);
}
return false;
},
};
-101
View File
@@ -1,101 +0,0 @@
import { createEventHandler } from "./events";
import type { VNode } from "./index";
type EventsSpec = { [name: string]: number };
type Catcher = (child: VNode, handlers: any[]) => VNode;
export function createCatcher(eventsSpec: EventsSpec): Catcher {
const n = Object.keys(eventsSpec).length;
class VCatcher {
child: VNode;
handlerData: any[];
handlerFns: any[] = [];
parentEl?: HTMLElement | undefined;
afterNode: Text | null = null;
constructor(child: VNode, handlers: any[]) {
this.child = child;
this.handlerData = handlers;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
this.child.mount(parent, afterNode);
this.afterNode = document.createTextNode("");
parent.insertBefore(this.afterNode, afterNode);
this.wrapHandlerData();
for (let name in eventsSpec) {
const index = eventsSpec[name];
const handler = createEventHandler(name);
this.handlerFns[index] = handler;
handler.setup.call(parent, this.handlerData[index]);
}
}
wrapHandlerData() {
for (let i = 0; i < n; i++) {
let handler = this.handlerData[i];
// handler = [...mods, fn, comp], so we need to replace second to last elem
let idx = handler.length - 2;
let origFn = handler[idx];
const self = this;
handler[idx] = function (ev: any) {
const target = ev.target;
for (let node of self.nodes()) {
if (node.contains(target)) {
return origFn.call(this, ev);
}
}
};
}
}
moveBefore(other: VCatcher | null, afterNode: Node | null) {
this.child.moveBefore(other ? other.child : null, afterNode);
this.parentEl!.insertBefore(this.afterNode!, afterNode);
}
patch(other: VCatcher, withBeforeRemove: boolean) {
if (this === other) {
return;
}
this.handlerData = other.handlerData;
this.wrapHandlerData();
for (let i = 0; i < n; i++) {
this.handlerFns[i].update.call(this.parentEl!, this.handlerData[i]);
}
this.child.patch(other.child, withBeforeRemove);
}
beforeRemove() {
this.child.beforeRemove();
}
remove() {
for (let i = 0; i < n; i++) {
this.handlerFns[i].remove.call(this.parentEl!);
}
this.child.remove();
this.afterNode!.remove();
}
firstNode(): Node | undefined {
return this.child.firstNode();
}
nodes(): Generator<Node> {
return this.child.nodes();
}
toString(): string {
return this.child.toString();
}
}
return function (child: VNode, handlers: any[]): VNode<VCatcher> {
return new VCatcher(child, handlers);
};
}
-100
View File
@@ -1,100 +0,0 @@
import { config } from "./config";
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
interface EventHandlerCreator {
setup: EventHandlerSetter;
update: EventHandlerSetter;
remove: (this: HTMLElement) => void;
}
export function createEventHandler(rawEvent: string): EventHandlerCreator {
const eventName = rawEvent.split(".")[0];
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 as HTMLElement;
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) 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 remove(this: HTMLElement) {
delete (this as any)[eventKey];
this.removeEventListener(evName, listener, { capture });
}
function update(this: HTMLElement, data: any) {
(this as any)[eventKey] = data;
}
return { setup, update, remove };
}
// 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;
}
function remove(this: HTMLElement) {
delete (this as any)[eventKey];
}
return { setup, update: setup, remove };
}
function nativeToSyntheticEvent(eventKey: string, event: Event) {
let dom = event.target;
while (dom !== null) {
const _data = (dom as any)[eventKey];
if (_data) {
for (const data of Object.values(_data)) {
const stopped = config.mainEventHandler(data, event, dom);
if (stopped) return;
}
}
dom = (dom as any).parentNode;
}
}
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) {
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
return;
}
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {
capture,
});
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
}
-94
View File
@@ -1,94 +0,0 @@
import type { VNode } from "./index";
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeRemoveChild = nodeProto.removeChild;
class VHtml {
html: string;
parentEl?: HTMLElement | undefined;
content: ChildNode[] = [];
constructor(html: string) {
this.html = html;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
const template = document.createElement("template");
template.innerHTML = this.html;
this.content = [...(template.content.childNodes as any)];
for (let elem of this.content) {
nodeInsertBefore.call(parent, elem, afterNode);
}
if (!this.content.length) {
const textNode = document.createTextNode("");
this.content.push(textNode);
nodeInsertBefore.call(parent, textNode, afterNode);
}
}
moveBefore(other: VHtml | null, afterNode: Node | null) {
const target = other ? other.content[0] : afterNode;
const parent = this.parentEl;
for (let elem of this.content) {
nodeInsertBefore.call(parent, elem, target);
}
}
patch(other: VHtml) {
if (this === other) {
return;
}
const html2 = other.html;
if (this.html !== html2) {
const parent = this.parentEl;
// insert new html in front of current
const afterNode = this.content[0];
const template = document.createElement("template");
template.innerHTML = html2;
const content = [...(template.content.childNodes as any)];
for (let elem of content) {
nodeInsertBefore.call(parent, elem, afterNode);
}
if (!content.length) {
const textNode = document.createTextNode("");
content.push(textNode);
nodeInsertBefore.call(parent, textNode, afterNode);
}
// remove current content
this.remove();
this.content = content;
this.html = other.html;
}
}
beforeRemove() {}
remove() {
const parent = this.parentEl;
for (let elem of this.content) {
nodeRemoveChild.call(parent, elem);
}
}
firstNode(): Node {
return this.content[0]!;
}
*nodes() {
for (let elem of this.content) {
yield elem;
}
}
toString() {
return this.html;
}
}
export function html(str: string): VNode<VHtml> {
return new VHtml(str);
}
-47
View File
@@ -1,47 +0,0 @@
export { config } from "./config";
export { toggler } from "./toggler";
export { createBlock } from "./block_compiler";
export { list } from "./list";
export { multi } from "./multi";
export { text, comment } from "./text";
export { html } from "./html";
export { createCatcher } from "./event_catcher";
export interface VNode<T = any> {
mount(parent: HTMLElement, afterNode: Node | null): void;
moveBefore(other: T | null, afterNode: Node | null): void;
patch(other: T, withBeforeRemove: boolean): void;
beforeRemove(): void;
remove(): void;
firstNode(): Node | undefined;
nodes(): Generator<Node>;
el?: undefined | HTMLElement | Text;
parentEl?: undefined | HTMLElement;
isOnlyChild?: boolean | undefined;
key?: any;
}
export type BDom = VNode<any>;
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
vnode.mount(fixture, afterNode);
}
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
vnode1.patch(vnode2, withBeforeRemove);
}
export function remove(vnode: VNode, withBeforeRemove: boolean = false) {
if (withBeforeRemove) {
vnode.beforeRemove();
}
vnode.remove();
}
export function withKey(vnode: VNode, key: any) {
vnode.key = key;
return vnode;
}
-245
View File
@@ -1,245 +0,0 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeAppendChild = nodeProto.appendChild;
const nodeRemoveChild = nodeProto.removeChild;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
// -----------------------------------------------------------------------------
// List Node
// -----------------------------------------------------------------------------
class VList {
children: VNode[];
anchor: Node | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
constructor(children: VNode[]) {
this.children = children;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const _anchor = document.createTextNode("");
this.anchor = _anchor;
nodeInsertBefore.call(parent, _anchor, afterNode);
const l = children.length;
if (l) {
const mount = children[0].mount;
for (let i = 0; i < l; i++) {
mount.call(children[i], parent, _anchor);
}
}
this.parentEl = parent;
}
moveBefore(other: VList | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchor) || null;
}
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].moveBefore(null, afterNode);
}
this.parentEl!.insertBefore(this.anchor!, afterNode);
}
patch(other: VList, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const ch1 = this.children;
const ch2: VNode[] = other.children;
if (ch2.length === 0 && ch1.length === 0) {
return;
}
this.children = ch2;
const proto = ch2[0] || ch1[0];
const {
mount: cMount,
patch: cPatch,
remove: cRemove,
beforeRemove,
moveBefore: cMoveBefore,
firstNode: cFirstNode,
} = proto;
const _anchor = this.anchor!;
const isOnlyChild = this.isOnlyChild;
const parent = this.parentEl!;
// fast path: no new child => only remove
if (ch2.length === 0 && isOnlyChild) {
if (withBeforeRemove) {
for (let i = 0, l = ch1.length; i < l; i++) {
beforeRemove.call(ch1[i]);
}
}
nodeSetTextContent.call(parent, "");
nodeAppendChild.call(parent, _anchor);
return;
}
let startIdx1 = 0;
let startIdx2 = 0;
let startVn1 = ch1[0];
let startVn2 = ch2[0];
let endIdx1 = ch1.length - 1;
let endIdx2 = ch2.length - 1;
let endVn1 = ch1[endIdx1];
let endVn2 = ch2[endIdx2];
let mapping: any = undefined;
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
// -------------------------------------------------------------------
if (startVn1 === null) {
startVn1 = ch1[++startIdx1];
continue;
}
// -------------------------------------------------------------------
if (endVn1 === null) {
endVn1 = ch1[--endIdx1];
continue;
}
// -------------------------------------------------------------------
let startKey1 = startVn1.key;
let startKey2 = startVn2.key;
if (startKey1 === startKey2) {
cPatch.call(startVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = startVn1;
startVn1 = ch1[++startIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
let endKey1 = endVn1.key;
let endKey2 = endVn2.key;
if (endKey1 === endKey2) {
cPatch.call(endVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = endVn1;
endVn1 = ch1[--endIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (startKey1 === endKey2) {
// bnode moved right
cPatch.call(startVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = startVn1;
const nextChild = ch2[endIdx2 + 1];
cMoveBefore.call(startVn1, nextChild, _anchor);
startVn1 = ch1[++startIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (endKey1 === startKey2) {
// bnode moved left
cPatch.call(endVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = endVn1;
const nextChild = ch1[startIdx1];
cMoveBefore.call(endVn1, nextChild, _anchor);
endVn1 = ch1[--endIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
mapping = mapping || createMapping(ch1, startIdx1, endIdx1);
let idxInOld = mapping[startKey2];
if (idxInOld === undefined) {
cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);
} else {
const elmToMove = ch1[idxInOld];
cMoveBefore.call(elmToMove, startVn1, null);
cPatch.call(elmToMove, startVn2, withBeforeRemove);
ch2[startIdx2] = elmToMove;
ch1[idxInOld] = null as any;
}
startVn2 = ch2[++startIdx2];
}
// ---------------------------------------------------------------------
if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {
if (startIdx1 > endIdx1) {
const nextChild = ch2[endIdx2 + 1];
const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;
for (let i = startIdx2; i <= endIdx2; i++) {
cMount.call(ch2[i], parent, anchor);
}
} else {
for (let i = startIdx1; i <= endIdx1; i++) {
let ch = ch1[i];
if (ch) {
if (withBeforeRemove) {
beforeRemove.call(ch);
}
cRemove.call(ch);
}
}
}
}
}
beforeRemove() {
const children = this.children;
const l = children.length;
if (l) {
const beforeRemove = children[0].beforeRemove;
for (let i = 0; i < l; i++) {
beforeRemove.call(children[i]);
}
}
}
remove() {
const { parentEl, anchor } = this;
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const l = children.length;
if (l) {
const remove = children[0].remove;
for (let i = 0; i < l; i++) {
remove.call(children[i]);
}
}
nodeRemoveChild.call(parentEl, anchor!);
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : undefined;
}
*nodes() {
for (let child of this.children) {
yield* child.nodes();
}
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
}
}
export function list(children: VNode[]): VNode<VList> {
return new VList(children);
}
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
let mapping: any = {};
for (let i = startIdx1; i <= endIdx2; i++) {
mapping[ch1[i].key] = i;
}
return mapping;
}
-142
View File
@@ -1,142 +0,0 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
const nodeRemoveChild = nodeProto.removeChild;
// -----------------------------------------------------------------------------
// Multi NODE
// -----------------------------------------------------------------------------
export class VMulti {
children: (VNode | undefined)[];
anchors?: Node[] | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
constructor(children: (VNode | undefined)[]) {
this.children = children;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const l = children.length;
const anchors = new Array(l);
for (let i = 0; i < l; i++) {
let child = children[i];
if (child) {
child.mount(parent, afterNode);
} else {
const childAnchor = document.createTextNode("");
anchors[i] = childAnchor;
nodeInsertBefore.call(parent, childAnchor, afterNode);
}
}
this.anchors = anchors;
this.parentEl = parent;
}
moveBefore(other: VMulti | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
}
const children = this.children;
const parent = this.parentEl;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
if (child) {
child.moveBefore(null, afterNode);
} else {
const anchor = anchors![i];
nodeInsertBefore.call(parent, anchor, afterNode);
}
}
}
patch(other: VMulti, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const children1 = this.children;
const children2 = other.children;
const anchors = this.anchors!;
const parentEl = this.parentEl!;
for (let i = 0, l = children1.length; i < l; i++) {
const vn1 = children1[i];
const vn2 = children2[i];
if (vn1) {
if (vn2) {
vn1.patch(vn2, withBeforeRemove);
} else {
const afterNode = vn1.firstNode()!;
const anchor = document.createTextNode("");
anchors[i] = anchor;
nodeInsertBefore.call(parentEl, anchor, afterNode);
if (withBeforeRemove) {
vn1.beforeRemove();
}
vn1.remove();
children1[i] = undefined;
}
} else if (vn2) {
children1[i] = vn2;
const anchor = anchors[i];
vn2.mount(parentEl, anchor);
nodeRemoveChild.call(parentEl, anchor);
}
}
}
beforeRemove() {
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.beforeRemove();
}
}
}
remove() {
const parentEl = this.parentEl;
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.remove();
} else {
nodeRemoveChild.call(parentEl, anchors![i]);
}
}
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : this.anchors![0];
}
*nodes() {
for (let child of this.children) {
if (child) {
yield* child.nodes();
}
}
}
toString(): string {
return this.children.map((c) => (c ? c!.toString() : "")).join("");
}
}
export function multi(children: (VNode | undefined)[]): VNode<VMulti> {
return new VMulti(children);
}
-93
View File
@@ -1,93 +0,0 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const characterDataProto = CharacterData.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeRemoveChild = nodeProto.removeChild;
abstract class VSimpleNode {
text: string | String;
parentEl?: HTMLElement | undefined;
el?: any;
constructor(text: string | String) {
this.text = text;
}
mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
nodeInsertBefore.call(parent, node, afterNode);
this.el = node;
}
moveBefore(other: VText | null, afterNode: Node | null) {
const target = other ? other.el! : afterNode;
nodeInsertBefore.call(this.parentEl, this.el!, target);
}
beforeRemove() {}
remove() {
nodeRemoveChild.call(this.parentEl, this.el!);
}
firstNode(): Node {
return this.el!;
}
*nodes(): Generator<Node> {
if (this.el) {
yield this.el;
}
}
toString() {
return this.text;
}
}
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 | String): VNode<VText> {
return new VText(str);
}
export function comment(str: string): VNode<VComment> {
return new VComment(str);
}
export function toText(value: any): string {
switch (typeof value) {
case "string":
return value;
case "number":
return String(value);
case "boolean":
return value ? "true" : "false";
default:
return value || "";
}
}
-69
View File
@@ -1,69 +0,0 @@
import type { VNode } from "./index";
// -----------------------------------------------------------------------------
// Toggler node
// -----------------------------------------------------------------------------
class VToggler {
key: string;
child: VNode;
parentEl?: HTMLElement | undefined;
constructor(key: string, child: VNode) {
this.key = key;
this.child = child;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
this.child.mount(parent, afterNode);
}
moveBefore(other: VToggler | null, afterNode: Node | null) {
this.child.moveBefore(other ? other.child : null, afterNode);
}
patch(other: VToggler, withBeforeRemove: boolean) {
if (this === other) {
return;
}
let child1 = this.child;
let child2 = other.child;
if (this.key === other.key) {
child1.patch(child2, withBeforeRemove);
} else {
child2.mount(this.parentEl!, child1.firstNode()!);
if (withBeforeRemove) {
child1.beforeRemove();
}
child1.remove();
this.child = child2;
this.key = other.key;
}
}
beforeRemove() {
this.child.beforeRemove();
}
remove() {
this.child.remove();
}
firstNode(): Node | undefined {
return this.child.firstNode();
}
nodes() {
return this.child.nodes();
}
toString(): string {
return this.child.toString();
}
}
export function toggler(key: string, child: VNode): VNode<VToggler> {
return new VToggler(key, child);
}
-44
View File
@@ -1,44 +0,0 @@
import { Schema } from "./validation";
import type { ComponentNode } from "./component_node";
// -----------------------------------------------------------------------------
// Component Class
// -----------------------------------------------------------------------------
export type Props = { [key: string]: any };
interface StaticComponentProperties {
template: string;
defaultProps?: any;
props?: Schema;
components?: { [componentName: string]: ComponentConstructor };
}
export type ComponentConstructor<P extends Props = any, E = any> = (new (
props: P,
env: E,
node: ComponentNode
) => Component<P, E>) &
StaticComponentProperties;
export class Component<Props = any, Env = any> {
static template: string = "";
static props?: any;
static defaultProps?: any;
props: Props;
env: Env;
__owl__: ComponentNode;
constructor(props: Props, env: Env, node: ComponentNode) {
this.props = props;
this.env = env;
this.__owl__ = node;
}
setup() {}
render(deep: boolean = false) {
this.__owl__.render(deep === true);
}
}
-359
View File
@@ -1,359 +0,0 @@
import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom";
import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError, OwlError } from "./error_handling";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import {
clearReactivesForCallback,
getSubscriptions,
NonReactive,
Reactive,
reactive,
TARGET,
} from "./reactivity";
import { STATUS } from "./status";
import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
}
return currentNode;
}
export function useComponent(): Component {
return currentNode!.component;
}
/**
* Apply default props (only top level).
*/
function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>) {
for (let propName in defaultProps) {
if (props[propName] === undefined) {
(props as any)[propName] = defaultProps[propName];
}
}
}
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
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> | NonReactive<T> {
const node = getCurrent();
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node, false));
batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
}
return reactive(state, render);
}
// -----------------------------------------------------------------------------
// Component VNode class
// -----------------------------------------------------------------------------
type LifecycleHook = Function;
export class ComponentNode<P extends Props = any, E = any> implements VNode<ComponentNode<P, E>> {
el?: HTMLElement | Text | undefined;
app: App;
fiber: Fiber | null = null;
component: Component<P, E>;
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
forceNextRender: boolean = false;
parentKey: string | null;
props: P;
renderFn: Function;
parent: ComponentNode | null;
childEnv: Env;
children: { [key: string]: ComponentNode } = Object.create(null);
refs: any = {};
willStart: LifecycleHook[] = [];
willUpdateProps: LifecycleHook[] = [];
willUnmount: LifecycleHook[] = [];
mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
constructor(
C: ComponentConstructor<P, E>,
props: P,
app: App,
parent: ComponentNode | null,
parentKey: string | null
) {
currentNode = this;
this.app = app;
this.parent = parent;
this.props = props;
this.parentKey = parentKey;
const defaultProps = C.defaultProps;
props = Object.assign({}, props);
if (defaultProps) {
applyDefaultProps(props, defaultProps);
}
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && prop[TARGET]) {
props[key] = useState(prop);
}
}
this.component = new C(props, env, this);
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
currentNode = null;
}
mountComponent(target: any, options?: MountOptions) {
const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber);
}
async initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) {
fiber.root!.mounted.push(fiber);
}
const component = this.component;
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
this.app.handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
fiber.render();
}
}
async render(deep: boolean) {
let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
}
if (current) {
if (!current.bdom && !fibersInError.has(current)) {
if (deep) {
// we want the render from this point on to be with deep=true
current.deep = deep;
}
return;
}
// if current rendering was with deep=true, we want this one to be the same
deep = deep || current.deep;
} else if (!this.bdom) {
return;
}
const fiber = makeRootFiber(this);
fiber.deep = deep;
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
return;
}
// We only want to actually render the component if the following two
// 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 === fiber && (current || !fiber.parent)) {
fiber.render();
}
}
destroy() {
let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy();
if (shouldRemove) {
this.bdom!.remove();
}
}
_destroy() {
const component = this.component;
if (this.status === STATUS.MOUNTED) {
for (let cb of this.willUnmount) {
cb.call(component);
}
}
for (let child of Object.values(this.children)) {
child._destroy();
}
if (this.willDestroy.length) {
try {
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
this.app.handleError({ error: e, node: this });
}
}
this.status = STATUS.DESTROYED;
}
async updateAndRender(props: P, parentFiber: Fiber) {
const rawProps = props;
props = Object.assign({}, props);
// update
const fiber = makeChildFiber(this, parentFiber);
this.fiber = fiber;
const component = this.component;
const defaultProps = (component.constructor as any).defaultProps;
if (defaultProps) {
applyDefaultProps(props, defaultProps);
}
currentNode = this;
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && prop[TARGET]) {
props[key] = useState(prop);
}
}
currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
component.props = props;
this.props = rawProps;
fiber.render();
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;
}
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
firstNode(): Node | undefined {
const bdom = this.bdom;
return bdom ? bdom.firstNode() : undefined;
}
*nodes(): Generator<Node> {
if (this.bdom) {
yield* this.bdom.nodes();
}
}
mount(parent: HTMLElement, anchor: ChildNode) {
const bdom = this.fiber!.bdom!;
this.bdom = bdom;
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.children = this.fiber!.childrenMap;
this.fiber = null;
}
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
}
patch() {
if (this.fiber && this.fiber.parent) {
// we only patch here renderings coming from above. renderings initiated
// by the component will be patched independently in the appropriate
// fiber.complete
this._patch();
}
}
_patch() {
let hasChildren = false;
for (let _k in this.children) {
hasChildren = true;
break;
}
const fiber = this.fiber!;
this.children = fiber.childrenMap;
this.bdom!.patch(fiber.bdom!, hasChildren);
fiber.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
this._destroy();
}
remove() {
this.bdom!.remove();
}
// ---------------------------------------------------------------------------
// Some debug helpers
// ---------------------------------------------------------------------------
get name(): string {
return this.component.constructor.name;
}
get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : [];
}
}
-76
View File
@@ -1,76 +0,0 @@
import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers";
// Custom error class that wraps error that happen in the owl lifecycle
export class OwlError extends Error {
cause?: any;
}
// 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): boolean {
if (!node) {
return false;
}
const fiber = node.fiber;
if (fiber) {
fibersInError.set(fiber, error);
}
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let handled = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
handled = true;
break;
} catch (e) {
error = e;
}
}
if (handled) {
return true;
}
}
return _handleError(node.parent, error);
}
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) {
let { error } = params;
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
if (!(error instanceof OwlError)) {
error = Object.assign(
new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`),
{ cause: 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);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
node.app.destroy();
} catch (e) {
console.error(e);
}
throw error;
}
}
-45
View File
@@ -1,45 +0,0 @@
import { filterOutModifiersFromData } from "./blockdom/config";
import { STATUS } from "./status";
import { OwlError } from "./error_handling";
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
data = _data;
let stopped = false;
if (modifiers.length) {
let selfMode = false;
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)) {
const handler = data[0];
if (typeof handler !== "function") {
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
}
let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === STATUS.MOUNTED : true) {
handler.call(node ? node.component : null, ev);
}
}
return stopped;
};
-265
View File
@@ -1,265 +0,0 @@
import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node";
import { fibersInError, OwlError } from "./error_handling";
import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
cancelFibers(current.children);
current.root = null;
}
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root!;
// lock root fiber because canceling children fibers may destroy components,
// which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings
root.locked = true;
root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false;
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
}
return current;
}
const fiber = new RootFiber(node, null);
if (node.willPatch.length) {
fiber.willPatch.push(fiber);
}
if (node.patched.length) {
fiber.patched.push(fiber);
}
return fiber;
}
function throwOnRender() {
throw new OwlError("Attempted to render cancelled fiber");
}
/**
* @returns number of not-yet rendered fibers cancelled
*/
function cancelFibers(fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
let node = fiber.node;
fiber.render = throwOnRender;
if (node.status === STATUS.NEW) {
node.destroy();
delete node.parent!.children[node.parentKey!];
}
node.fiber = null;
if (fiber.bdom) {
// if fiber has been rendered, this means that the component props have
// been updated. however, this fiber will not be patched to the dom, so
// it could happen that the next render compare the current props with
// the same props, and skip the render completely. With the next line,
// we kindly request the component code to force a render, so it works as
// expected.
node.forceNextRender = true;
} else {
result++;
}
result += cancelFibers(fiber.children);
}
return result;
}
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber | null; // A Fiber that has been replaced by another has no root
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
this.parent = parent;
if (parent) {
this.deep = parent.deep;
const root = parent.root!;
root.setCounter(root.counter + 1);
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
render() {
// if some parent has a fiber => register in followup
let prev = this.root!.node;
let scheduler = prev.app.scheduler;
let current = prev.parent;
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
}
}
prev = current;
current = current.parent;
}
// there are no current rendering from above => we can render
this._render();
}
_render() {
const node = this.node;
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
node.app.handleError({ node, error: e });
}
root.setCounter(root.counter - 1);
}
}
}
export class RootFiber extends Fiber {
counter: number = 1;
// only add stuff in this if they have registered some hooks
willPatch: Fiber[] = [];
patched: 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
locked: boolean = false;
complete() {
const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
// because of the asynchronous nature of the rendering, some parts of the
// UI may have been rendered, then deleted in a followup rendering, and we
// do not want to call onWillPatch in that case.
let node = current.node;
if (node.fiber === current) {
const component = node.component;
for (let cb of node.willPatch) {
cb.call(component);
}
}
}
current = undefined;
// Step 2: patching the dom
node._patch();
this.locked = false;
// 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;
node.app.handleError({ fiber: current || this, error: e });
}
}
setCounter(newValue: number) {
this.counter = newValue;
if (newValue === 0) {
this.node.app.scheduler.flush();
}
}
}
type Position = "first-child" | "last-child";
export interface MountOptions {
position?: Position;
}
export class MountFiber extends RootFiber {
target: HTMLElement;
position: Position;
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
super(node, null);
this.target = target;
this.position = options.position || "last-child";
}
complete() {
let current: Fiber | undefined = this;
try {
const node = this.node;
node.children = this.childrenMap;
(node.app.constructor as any).validateTarget(this.target);
if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing
// bdom, this means that this same fiber was already completed, mounted,
// but a crash occurred in some mounted hook. Then, it was handled and
// the new rendering is being applied.
node.updateDom();
} else {
node.bdom = this.bdom;
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) {
this.node.app.handleError({ fiber: current as Fiber, error: e });
}
}
}
-165
View File
@@ -1,165 +0,0 @@
import type { Env } from "./app";
import { getCurrent } from "./component_node";
import { onMounted, onPatched, onWillUnmount } from "./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;
}
function extendEnv(currentEnv: Object, extension: Object): Object {
const env = Object.create(currentEnv);
const descrs = Object.getOwnPropertyDescriptors(extension);
return Object.freeze(Object.defineProperties(env, descrs));
}
/**
* 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();
node.component.env = extendEnv(node.component.env as any, envExtension);
useChildSubEnv(envExtension);
}
export function useChildSubEnv(envExtension: Env) {
const node = getCurrent();
node.childEnv = extendEnv(node.childEnv, envExtension);
}
// -----------------------------------------------------------------------------
// useEffect
// -----------------------------------------------------------------------------
/**
* @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) | void;
let dependencies: any[];
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies);
});
onPatched(() => {
const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
if (cleanup) {
cleanup();
}
cleanup = effect(...dependencies);
}
});
onWillUnmount(() => cleanup && 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));
}
// -----------------------------------------------------------------------------
// useRoots
// -----------------------------------------------------------------------------
interface DomRangeObj {
node: Node | null;
elem: HTMLElement | null;
elems: Iterable<HTMLElement>;
nodes: Iterable<Node>;
}
export function useRoots(): DomRangeObj {
const cnode = getCurrent();
function* _elems(): Generator<HTMLElement> {
for (let node of cnode.nodes()) {
if (node.nodeType === 1) {
yield node as HTMLElement;
}
}
}
return {
get node() {
return cnode.nodes().next().value || null;
},
get elem() {
return _elems().next().value || null;
},
get nodes() {
return cnode.nodes();
},
get elems() {
return _elems();
},
};
}
-68
View File
@@ -1,68 +0,0 @@
import {
config,
createBlock,
html,
list,
mount as blockMount,
multi,
patch,
remove,
text,
toggler,
comment,
} from "./blockdom";
import { mainEventHandler } from "./event_handling";
export type { Reactive } from "./reactivity";
config.shouldNormalizeDom = false;
config.mainEventHandler = mainEventHandler;
export const blockDom = {
config,
// bdom entry points
mount: blockMount,
patch,
remove,
// bdom block types
list,
multi,
text,
toggler,
createBlock,
html,
comment,
};
export { App, mount } from "./app";
export { xml } from "./template_set";
export { Component } from "./component";
export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node";
export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity";
export {
useEffect,
useEnv,
useExternalListener,
useRef,
useChildSubEnv,
useSubEnv,
useRoots,
} from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onWillPatch,
onPatched,
onWillRender,
onRendered,
onWillDestroy,
onError,
} from "./lifecycle_hooks";
export { validate } from "./validation";
export { OwlError } from "./error_handling";
export const __info__ = {};
-122
View File
@@ -1,122 +0,0 @@
import { getCurrent } from "./component_node";
import { nodeErrorHandlers, OwlError } from "./error_handling";
const TIMEOUT = Symbol("timeout");
function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
cause: any;
};
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
const node = getCurrent();
return (...args: any[]) => {
const onError = (cause: any) => {
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
} else {
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
}
throw error;
};
try {
const result = fn(...args);
if (result instanceof Promise) {
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
const fiber = node.fiber;
Promise.race([
result.catch(() => {}),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber) {
console.warn(timeoutError);
}
});
}
return result.catch(onError);
}
return result;
} catch (cause) {
onError(cause);
}
};
}
// -----------------------------------------------------------------------------
// hooks
// -----------------------------------------------------------------------------
export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
}
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
}
export function onMounted(fn: () => void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
}
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
}
export function onPatched(fn: () => void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
}
export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
}
export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent();
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
}
export function onWillRender(fn: () => void | any) {
const node = getCurrent();
const renderFn = node.renderFn;
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
fn = decorate(fn.bind(node.component), "onWillRender");
node.renderFn = () => {
fn();
return renderFn();
};
}
export function onRendered(fn: () => void | any) {
const node = getCurrent();
const renderFn = node.renderFn;
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
fn = decorate(fn.bind(node.component), "onRendered");
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.bind(node.component));
}
-85
View File
@@ -1,85 +0,0 @@
import { onWillUnmount } from "./lifecycle_hooks";
import { BDom, text, VNode } from "./blockdom";
import { Component } from "./component";
import { OwlError } from "./error_handling";
const VText: any = text("").constructor;
class VPortal extends VText implements Partial<VNode<VPortal>> {
// selector: string;
realBDom: BDom | null;
target: HTMLElement | null = null;
constructor(selector: string, realBDom: BDom) {
super("");
this.selector = selector;
this.realBDom = realBDom;
}
mount(parent: HTMLElement, anchor: ChildNode) {
super.mount(parent, anchor);
this.target = document.querySelector(this.selector) as any;
if (!this.target) {
let el: any = this.el;
while (el && el.parentElement instanceof HTMLElement) {
el = el.parentElement;
}
this.target = el && el.querySelector(this.selector);
if (!this.target) {
throw new OwlError("invalid portal target");
}
}
this.realBDom!.mount(this.target!, null);
}
beforeRemove() {
this.realBDom!.beforeRemove();
}
remove() {
if (this.realBDom) {
super.remove();
this.realBDom!.remove();
this.realBDom = null;
}
}
patch(other: VPortal) {
super.patch(other);
if (this.realBDom) {
this.realBDom.patch(other.realBDom!, true);
} else {
this.realBDom = other.realBDom;
this.realBDom!.mount(this.target!, null);
}
}
}
/**
* <t t-slot="default"/>
*/
export function portalTemplate(app: any, bdom: any, helpers: any) {
let { callSlot } = helpers;
return function template(ctx: any, node: any, key = "") {
return callSlot(ctx, node, key, "default", false, null);
};
}
export class Portal extends Component {
static template = "__portal__";
static props = {
target: {
type: String,
},
slots: true,
};
setup() {
const node = this.__owl__;
const renderFn = node.renderFn;
node.renderFn = () => new VPortal(this.props.target, renderFn());
onWillUnmount(() => {
if (node.bdom) {
node.bdom.remove();
}
});
}
}
-458
View File
@@ -1,458 +0,0 @@
import { Callback } from "./utils";
import { OwlError } from "./error_handling";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
export const TARGET = Symbol("Target");
// Escape hatch to prevent reactivity system to turn something into a reactive
const SKIP = Symbol("Skip");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
type Target = object;
type Collection = Set<any> | Map<any, any> | WeakMap<any, any>;
type CollectionRawType = "Set" | "Map" | "WeakMap";
export type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
export type NonReactive<T extends Target = Target> = T & {
[SKIP]: any;
};
const objectToString = Object.prototype.toString;
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
/**
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
* many native objects such as Promise (whose toString is [object Promise])
* or Date ([object Date]), while also supporting collections without using
* instanceof in a loop
*
* @param obj the object to check
* @returns the raw type of the object
*/
function rawType(obj: any) {
return objectToString.call(obj).slice(8, -1);
}
/**
* Checks whether a given value can be made into a reactive object.
*
* @param value the value to check
* @returns whether the value can be made reactive
*/
function canBeMadeReactive(value: any): boolean {
if (typeof value !== "object") {
return false;
}
return SUPPORTED_RAW_TYPES.has(rawType(value));
}
/**
* Creates a reactive from the given object/callback if possible and returns it,
* returns the original object otherwise.
*
* @param value the value make reactive
* @returns a reactive for the given object when possible, the original otherwise
*/
function possiblyReactive(val: any, cb: Callback) {
return canBeMadeReactive(val) ? reactive(val, cb) : val;
}
/**
* Mark an object or array so that it is ignored by the reactivity system
*
* @param value the value to mark
* @returns the object itself
*/
export function markRaw<T extends Target>(value: T): NonReactive<T> {
(value as any)[SKIP] = true;
return value as NonReactive<T>;
}
/**
* Given a reactive objet, return the raw (non reactive) underlying object
*
* @param value a reactive value
* @returns the underlying value
*/
export function toRaw<T extends object>(value: Reactive<T>): T {
return value[TARGET] || value;
}
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
/**
* Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target.
*
* @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: PropertyKey, callback: Callback): void {
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
keyToCallbacks.get(key)!.add(callback);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
/**
* Notify Reactives that are observing a given target that a key has changed on
* the target.
*
* @param target target whose Reactives should be notified that the target was
* changed.
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted)
*/
function notifyReactives(target: Target, key: PropertyKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToCallbacks) {
return;
}
const callbacks = keyToCallbacks.get(key);
if (!callbacks) {
return;
}
// Loop on copy because clearReactivesForCallback will modify the set in place
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
}
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
export 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();
}
export function getSubscriptions(callback: Callback) {
const targets = callbacksToTargets.get(callback) || [];
return [...targets].map((target) => {
const keysToCallbacks = targetToKeysToCallbacks.get(target);
return {
target,
keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
};
});
}
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> | NonReactive<T> {
if (!canBeMadeReactive(target)) {
throw new OwlError(`Cannot make the given value reactive`);
}
if (SKIP in target) {
return target as NonReactive<T>;
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new WeakMap());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const targetRawType = rawType(target);
const handler = COLLECTION_RAWTYPES.has(targetRawType)
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
: basicProxyHandler<T>(callback);
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
reactivesForTarget.set(callback, proxy);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
/**
* Creates a basic proxy handler for regular objects and arrays.
*
* @param callback @see reactive
* @returns a proxy handler object
*/
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
return {
get(target: any, key: PropertyKey, proxy: Reactive<T>) {
if (key === TARGET) {
return target;
}
// non-writable non-configurable properties cannot be made reactive
const desc = Object.getOwnPropertyDescriptor(target, key);
if (desc && !desc.writable && !desc.configurable) {
return Reflect.get(target, key, proxy);
}
observeTargetKey(target, key, callback);
return possiblyReactive(Reflect.get(target, key, proxy), callback);
},
set(target, key, value, proxy) {
const isNewKey = !objectHasOwnProperty.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);
// TODO: only notify when something was actually deleted
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
// observing the key itself would observe value changes instead of presence changes
// so we may need a finer grained system to distinguish observing value vs presence.
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key);
},
} as ProxyHandler<T>;
}
/**
* Creates a function that will observe the key that is passed to it when called
* and delegates to the underlying method.
*
* @param methodName name of the method to delegate to
* @param target @see reactive
* @param callback @see reactive
*/
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
return (key: any) => {
key = toRaw(key);
observeTargetKey(target, key, callback);
return possiblyReactive(target[methodName](key), callback);
};
}
/**
* Creates an iterable that will delegate to the underlying iteration method and
* observe keys as necessary.
*
* @param methodName name of the method to delegate to
* @param target @see reactive
* @param callback @see reactive
*/
function makeIteratorObserver(
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
target: any,
callback: Callback
) {
return function* () {
observeTargetKey(target, KEYCHANGES, callback);
const keys = target.keys();
for (const item of target[methodName]()) {
const key = keys.next().value;
observeTargetKey(target, key, callback);
yield possiblyReactive(item, callback);
}
};
}
/**
* Creates a forEach function that will delegate to forEach on the underlying
* collection while observing key changes, and keys as they're iterated over,
* and making the passed keys/values reactive.
*
* @param target @see reactive
* @param callback @see reactive
*/
function makeForEachObserver(target: any, callback: Callback) {
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
observeTargetKey(target, KEYCHANGES, callback);
target.forEach(function (val: any, key: any, targetObj: any) {
observeTargetKey(target, key, callback);
forEachCb.call(
thisArg,
possiblyReactive(val, callback),
possiblyReactive(key, callback),
possiblyReactive(targetObj, callback)
);
}, thisArg);
};
}
/**
* Creates a function that will delegate to an underlying method, and check if
* that method has modified the presence or value of a key, and notify the
* reactives appropriately.
*
* @param setterName name of the method to delegate to
* @param getterName name of the method which should be used to retrieve the
* value before calling the delegate method for comparison purposes
* @param target @see reactive
*/
function delegateAndNotify(
setterName: "set" | "add" | "delete",
getterName: "has" | "get",
target: any
) {
return (key: any, value: any) => {
key = toRaw(key);
const hadKey = target.has(key);
const originalValue = target[getterName](key);
const ret = target[setterName](key, value);
const hasKey = target.has(key);
if (hadKey !== hasKey) {
notifyReactives(target, KEYCHANGES);
}
if (originalValue !== value) {
notifyReactives(target, key);
}
return ret;
};
}
/**
* Creates a function that will clear the underlying collection and notify that
* the keys of the collection have changed.
*
* @param target @see reactive
*/
function makeClearNotifier(target: Map<any, any> | Set<any>) {
return () => {
const allKeys = [...target.keys()];
target.clear();
notifyReactives(target, KEYCHANGES);
for (const key of allKeys) {
notifyReactives(target, key);
}
};
}
/**
* Maps raw type of an object to an object containing functions that can be used
* to build an appropritate proxy handler for that raw type. Eg: when making a
* reactive set, calling the has method should mark the key that is being
* retrieved as observed, and calling the add or delete method should notify the
* reactives that the key which is being added or deleted has been modified.
*/
const rawTypeToFuncHandlers = {
Set: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
add: delegateAndNotify("add", "has", target),
delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
observeTargetKey(target, KEYCHANGES, callback);
return target.size;
},
}),
Map: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback),
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
observeTargetKey(target, KEYCHANGES, callback);
return target.size;
},
}),
WeakMap: (target: any, callback: Callback) => ({
has: makeKeyObserver("has", target, callback),
get: makeKeyObserver("get", target, callback),
set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target),
}),
};
/**
* Creates a proxy handler for collections (Set/Map/WeakMap)
*
* @param callback @see reactive
* @param target @see reactive
* @returns a proxy handler object
*/
function collectionsProxyHandler<T extends Collection>(
target: T,
callback: Callback,
targetRawType: CollectionRawType
): ProxyHandler<T> {
// TODO: if performance is an issue we can create the special handlers lazily when each
// property is read.
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
return Object.assign(basicProxyHandler(callback), {
get(target: any, key: PropertyKey) {
if (key === TARGET) {
return target;
}
if (objectHasOwnProperty.call(specialHandlers, key)) {
return (specialHandlers as any)[key];
}
observeTargetKey(target, key, callback);
return possiblyReactive(target[key], callback);
},
}) as ProxyHandler<T>;
}
-76
View File
@@ -1,76 +0,0 @@
import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status";
// -----------------------------------------------------------------------------
// Scheduler
// -----------------------------------------------------------------------------
export class Scheduler {
// capture the value of requestAnimationFrame as soon as possible, to avoid
// interactions with other code, such as test frameworks that override them
static requestAnimationFrame = window.requestAnimationFrame.bind(window);
tasks: Set<RootFiber> = new Set();
requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0;
delayedRenders: Fiber[] = [];
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root!);
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
*/
flush() {
if (this.delayedRenders.length) {
let renders = this.delayedRenders;
this.delayedRenders = [];
for (let f of renders) {
if (f.root && f.node.status !== STATUS.DESTROYED && f.node.fiber === f) {
f.render();
}
}
}
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => {
this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber));
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
}
});
}
}
processFiber(fiber: RootFiber) {
if (fiber.root !== fiber) {
this.tasks.delete(fiber);
return;
}
const hasError = fibersInError.has(fiber);
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber);
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!hasError) {
fiber.complete();
}
this.tasks.delete(fiber);
}
}
}
-24
View File
@@ -1,24 +0,0 @@
import type { Component } from "./component";
// -----------------------------------------------------------------------------
// Status
// -----------------------------------------------------------------------------
export const enum STATUS {
NEW,
MOUNTED, // is ready, and in DOM. It has a valid el
DESTROYED,
}
type STATUS_DESCR = "new" | "mounted" | "destroyed";
export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) {
case STATUS.NEW:
return "new";
case STATUS.MOUNTED:
return "mounted";
case STATUS.DESTROYED:
return "destroyed";
}
}
-271
View File
@@ -1,271 +0,0 @@
import { BDom, multi, text, toggler, createCatcher } from "./blockdom";
import { Markup } from "./utils";
import { html } from "./blockdom/index";
import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity";
import { OwlError } from "./error_handling";
const ObjectCreate = Object.create;
/**
* 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 callSlot(
ctx: any,
parent: any,
key: string,
name: string,
dynamic: boolean,
extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
key = key + "__slot_" + name;
const slots = ctx.props.slots || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = ObjectCreate(__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 = ObjectCreate(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 OwlError("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;
component: any;
node: any;
constructor(fn: any, ctx: any, component: any, node: any) {
this.fn = fn;
this.ctx = capture(ctx);
this.component = component;
this.node = node;
}
evaluate(): any {
return this.fn.call(this.component, 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, defaultValue?: any): ReturnType<typeof toggler> {
if (value === undefined) {
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
}
let safeKey;
let block;
switch (typeof value) {
case "object":
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 (value instanceof String) {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
break;
case "string":
safeKey = "string_unsafe";
block = text(value);
break;
default:
safeKey = "string_unsafe";
block = text(String(value));
}
return toggler(safeKey, block);
}
let boundFunctions = new WeakMap();
const WeakMapGet = WeakMap.prototype.get;
const WeakMapSet = WeakMap.prototype.set;
function bind(ctx: any, fn: Function): Function {
let component = ctx.__owl__.component;
let boundFnMap = WeakMapGet.call(boundFunctions, component);
if (!boundFnMap) {
boundFnMap = new WeakMap();
WeakMapSet.call(boundFunctions, component, boundFnMap);
}
let boundFn = WeakMapGet.call(boundFnMap, fn);
if (!boundFn) {
boundFn = fn.bind(component);
WeakMapSet.call(boundFnMap, 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 OwlError("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
refs[name] = el;
}
};
}
/**
* 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 function validateProps<P>(name: string | ComponentConstructor<P>, props: P, node?: any) {
const ComponentClass =
typeof name !== "string"
? name
: (node.component.constructor.components[name] as ComponentConstructor<P> | undefined);
if (!ComponentClass) {
// this is an error, wrong component. We silently return here instead so the
// error is triggered by the usual path ('component' function)
return;
}
const schema = ComponentClass.props;
if (!schema) {
if (node.app.warnIfNoStaticProps) {
console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
}
return;
}
const defaultProps = ComponentClass.defaultProps;
if (defaultProps) {
let isMandatory = (name: string) =>
Array.isArray(schema)
? schema.includes(name)
: name in schema && !("*" in schema) && !isOptional(schema[name]);
for (let p in defaultProps) {
if (isMandatory(p)) {
throw new OwlError(
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
);
}
}
}
const errors = validateSchema(props, schema);
if (errors.length) {
throw new OwlError(
`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
);
}
}
export const helpers = {
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
capture,
withKey,
prepareList,
setContextValue,
multiRefSetter,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
bind,
createCatcher,
markRaw,
OwlError,
};
-146
View File
@@ -1,146 +0,0 @@
import { compile, Template, TemplateFunction } from "../compiler";
import { comment, createBlock, html, list, multi, text, toggler } from "./blockdom";
import { getCurrent } from "./component_node";
import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers";
import { OwlError } from "./error_handling";
const bdom = { text, createBlock, list, multi, html, toggler, comment };
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 OwlError(msg);
}
return doc;
}
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document;
}
export class TemplateSet {
static registerTemplate(name: string, fn: TemplateFunction) {
globalTemplates[name] = fn;
}
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
Portal = Portal;
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 | Element) {
if (name in this.rawTemplates) {
const rawTemplate = this.rawTemplates[name];
const currentAsString =
typeof rawTemplate === "string"
? rawTemplate
: rawTemplate instanceof Element
? rawTemplate.outerHTML
: rawTemplate.toString();
const newAsString = typeof template === "string" ? template : template.outerHTML;
if (currentAsString === newAsString) {
return;
}
throw new OwlError(`Template ${name} already defined with different content`);
}
this.rawTemplates[name] = template;
}
addTemplates(xml: string | Document) {
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")!;
this.addTemplate(name, template);
}
}
getTemplate(name: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
let extraInfo = "";
try {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
}
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
const templates = this.templates;
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(this, bdom, helpers);
this.templates[name] = template;
}
return this.templates[name];
}
_compileTemplate(name: string, template: string | Element): ReturnType<typeof compile> {
throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
}
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
}
}
// -----------------------------------------------------------------------------
// xml tag helper
// -----------------------------------------------------------------------------
export const globalTemplates: { [key: string]: string | Element | TemplateFunction } = {};
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;
TemplateSet.registerTemplate("__portal__", portalTemplate);
-82
View File
@@ -1,82 +0,0 @@
import { OwlError } from "./error_handling";
export type Callback = () => void;
/**
* 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;
// 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.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
}
};
}
export function validateTarget(target: HTMLElement) {
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
const HTMLElement = document.defaultView!.HTMLElement;
if (target instanceof HTMLElement) {
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
}
}
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
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 OwlError("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);
}
-169
View File
@@ -1,169 +0,0 @@
import { OwlError } from "./error_handling";
type BaseType =
| typeof String
| typeof Boolean
| typeof Number
| typeof Date
| typeof Object
| typeof Array
| true
| "*";
interface TypeInfo {
type?: TypeDescription;
optional?: boolean;
validate?: Function;
shape?: Schema;
element?: TypeDescription;
}
type ValueType = { value: any };
type TypeDescription = BaseType | TypeInfo | ValueType | TypeDescription[];
type SimplifiedSchema = string[];
type NormalizedSchema = { [key: string]: TypeDescription };
export type Schema = SimplifiedSchema | NormalizedSchema;
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
const isUnionType = (t: TypeDescription): t is TypeDescription[] => Array.isArray(t);
const isBaseType = (t: TypeDescription): t is BaseType => typeof t !== "object";
const isValueType = (t: TypeDescription): t is ValueType =>
typeof t === "object" && t && "value" in t;
export function isOptional(t: TypeDescription): Boolean {
return typeof t === "object" && "optional" in t ? t.optional || false : false;
}
function describeType(type: BaseType): string {
return type === "*" || type === true ? "value" : type.name.toLowerCase();
}
function describe(info: TypeDescription): string {
if (isBaseType(info)) {
return describeType(info);
} else if (isUnionType(info)) {
return info.map(describe).join(" or ");
} else if (isValueType(info)) {
return String(info.value);
}
if ("element" in info) {
return `list of ${describe({ type: info.element, optional: false })}s`;
}
if ("shape" in (info as TypeInfo)) {
return `object`;
}
return describe(info.type || "*");
}
function toSchema(spec: SimplifiedSchema): NormalizedSchema {
return Object.fromEntries(
spec.map((e) =>
e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]
)
);
}
/**
* Main validate function
*/
export function validate(obj: { [key: string]: any }, spec: Schema) {
let errors = validateSchema(obj, spec);
if (errors.length) {
throw new OwlError("Invalid object: " + errors.join(", "));
}
}
/**
* Helper validate function, to get the list of errors. useful if one want to
* manipulate the errors without parsing an error object
*/
export function validateSchema(obj: { [key: string]: any }, schema: Schema): string[] {
if (Array.isArray(schema)) {
schema = toSchema(schema);
}
let errors = [];
// check if each value in obj has correct shape
for (let key in obj) {
if (key in schema) {
let result = validateType(key, obj[key], schema[key]);
if (result) {
errors.push(result);
}
} else if (!("*" in schema)) {
errors.push(`unknown key '${key}'`);
}
}
// check that all specified keys are defined in obj
for (let key in schema) {
const spec = schema[key];
if (key !== "*" && !isOptional(spec) && !(key in obj)) {
const isObj = typeof spec === "object" && !Array.isArray(spec);
const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
errors.push(`'${key}' is missing${detail}`);
}
}
return errors;
}
function validateBaseType(key: string, value: any, type: BaseType): string | null {
if (typeof type === "function") {
if (typeof value === "object") {
if (!(value instanceof type)) {
return `'${key}' is not a ${describeType(type)}`;
}
} else if (typeof value !== type.name.toLowerCase()) {
return `'${key}' is not a ${describeType(type)}`;
}
}
return null;
}
function validateArrayType(key: string, value: any, descr: TypeDescription): string | null {
if (!Array.isArray(value)) {
return `'${key}' is not a list of ${describe(descr)}s`;
}
for (let i = 0; i < value.length; i++) {
const error = validateType(`${key}[${i}]`, value[i], descr);
if (error) {
return error;
}
}
return null;
}
function validateType(key: string, value: any, descr: TypeDescription): string | null {
if (value === undefined) {
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
} else if (isBaseType(descr)) {
return validateBaseType(key, value, descr);
} else if (isValueType(descr)) {
return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
} else if (isUnionType(descr)) {
let validDescr = descr.find((p) => !validateType(key, value, p));
return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
}
let result: string | null = null;
if ("element" in descr) {
result = validateArrayType(key, value, descr.element!);
} else if ("shape" in descr && !result) {
if (typeof value !== "object" || Array.isArray(value)) {
result = `'${key}' is not an object`;
} else {
const errors = validateSchema(value, descr.shape!);
if (errors.length) {
result = `'${key}' has not the correct shape (${errors.join(", ")})`;
}
}
}
if ("type" in descr && !result) {
result = validateType(key, value, descr.type!);
}
if ("validate" in descr && !result) {
result = !descr.validate!(value) ? `'${key}' is not valid` : null;
}
return result;
}
+147
View File
@@ -0,0 +1,147 @@
import { Component, Env, Fiber } from "../component/component";
import { VNode } from "../vdom/index";
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P> extends Component<T, P> {
deep: boolean = true;
getStore(env) {
return env.store;
}
storeProps: any;
hashFunction: HashFunction = (storeProps, options) => {
const revFn = (this.__owl__ as any).revFn;
const rev = revFn(storeProps);
if (rev > 0) {
return rev;
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = revFn(val);
if (hashVal === 0) {
if (val !== options.prevStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
dispatch(name, ...payload) {
return (this.__owl__ as any).store.dispatch(name, ...payload);
}
/**
* Need to do this here so 'deep' can be overrided by subcomponent easily
*/
async __prepareAndRender(fiber: Fiber<P>): Promise<VNode> {
const store = this.getStore(this.env);
const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
(this.__owl__ as any).store = store;
(this.__owl__ as any).ownProps = this.props;
(this.__owl__ as any).revFn = revFn.bind(observer);
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps
});
(this.__owl__ as any).rev = observer.rev;
return super.__prepareAndRender(fiber);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
__callWillUnmount() {
(this.__owl__ as any).store.off("update", this);
super.__callWillUnmount();
}
__destroy(parent: any) {
(this.__owl__ as any).store.off("update", this);
super.__destroy(parent);
}
async render(force: boolean = false) {
this.__updateStoreProps(this.props);
// this is quite technical, so this deserves some explanation.
// When we have a connected component, it can be updated for 3 reasons:
// - some internal state changes (this will go through this method)
// - some props changes (if a parent is changed and need to rerender itself)
// - a store update
//
// It is possible (with connected component and parent) to have the following
// situation: the parent component is rendered first (from its state change),
// then immediately after, it is rendered (from store update). Then, if the
// __checkUpdate method is immediately over, the children component will
// be rendered again by the store update, even though it is supposed to be
// destroyed by the first rendering.
//
// So, the solution is to keep the information that there is a current
// rendering occuring with the same store state, the same props, and return
// that in the __checkUpdate method. To do this, we use the renderPromise
// deferred, which is not used by the component system once the
// component is ready, so we can use it for our own purpose.
(this.__owl__ as any).renderPromise = super.render(force);
return (this.__owl__ as any).renderPromise;
}
async __updateProps(nextProps: P, f, s, v) {
this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, s, v);
}
__updateStoreProps(nextProps): boolean {
const __owl__ = this.__owl__ as any;
const store = __owl__.store;
const observer = store.observer;
if (observer.rev === __owl__.rev && nextProps === __owl__.ownProps) {
return false;
}
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
nextProps,
store.getters
);
const options = { prevStoreProps: this.storeProps, didChange: false };
const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps;
let didChange = options.didChange;
if (storeHash !== __owl__.storeHash) {
__owl__.storeHash = storeHash;
didChange = true;
}
__owl__.rev = store.observer.rev;
__owl__.ownProps = nextProps;
return didChange;
}
async __checkUpdate() {
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
}
// see note in render method
return (this.__owl__ as any).renderPromise;
}
}
View File
+107
View File
@@ -0,0 +1,107 @@
import { Env } from "../component/component";
import { EventBus } from "../core/event_bus";
import { Observer } from "../core/observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - the ConnectedComponent class
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.actions = config.actions;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(config.state || {});
this.getters = {};
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner);
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { QWeb } from "./qweb/index";
/**
* Owl Tags
*
* We have here a (very) small collection of tag functions:
*
* - xml
*
* The plan is to add a few other tags such as css, globalcss.
*/
/**
* XML tag helper for defining templates. With this, one can simply define
* an inline template with just the template xml:
* ```js
* class A extends Component {
* static template = xml`<div>some template</div>`;
* }
* ```
*/
export function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
return name;
}
+104
View File
@@ -0,0 +1,104 @@
/**
* Owl Utils
*
* We have here a small collection of utility functions:
*
* - whenReady
* - loadJS
* - loadTemplates
* - escape
* - debounce
*/
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
if (document.readyState !== "loading") {
resolve();
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function() {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise: Promise<void> = new Promise(function(resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
resolve();
};
script.onerror = function() {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}
export async function loadTemplates(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return await result.text();
}
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout;
return function(this: any) {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export function shallowEqual(p1, p2): boolean {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
+9
View File
@@ -0,0 +1,9 @@
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
import { init } from "./vdom";
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export { h, VNode } from "./vdom";
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+234
View File
@@ -0,0 +1,234 @@
import { Module, VNode, VNodeData } from "./vdom";
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
function updateProps(oldVnode: VNode, vnode: VNode): void {
var key: string,
cur: any,
old: any,
elm = vnode.elm,
oldProps = (oldVnode.data as VNodeData).props,
props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in oldProps) {
if (!props[key]) {
delete (elm as any)[key];
}
}
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule = {
create: updateProps,
update: updateProps
} as Module;
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call handler with arguments
if (typeof handler[0] === "function") {
// special case for single argument for performance
if (handler.length === 2) {
handler[0].call(vnode, handler[1], event, vnode);
} else {
var args = handler.slice(1);
args.push(event);
args.push(vnode);
handler[0].apply(vnode, args);
}
} else {
// call multiple handlers
for (let i = 0, iLen = handler.length; i < iLen; i++) {
invokeHandler(handler[i], vnode, event);
}
}
}
}
function handleEvent(event: Event, vnode: VNode) {
var name = event.type,
on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
}
}
function createListener() {
return function handler(event: Event) {
handleEvent(event, (handler as any).vnode);
};
}
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
var oldOn = (oldVnode.data as VNodeData).on,
oldListener = (oldVnode as any).listener,
oldElm: Element = oldVnode.elm as Element,
on = vnode && (vnode.data as VNodeData).on,
elm: Element = (vnode && vnode.elm) as Element,
name: string;
// optimization for reused immutable handlers
if (oldOn === on) {
return;
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
}
}
}
}
}
export const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
} as Module;
//------------------------------------------------------------------------------
// attributes.ts
//------------------------------------------------------------------------------
const xlinkNS = "http://www.w3.org/1999/xlink";
const xmlNS = "http://www.w3.org/XML/1998/namespace";
const colonChar = 58;
const xChar = 120;
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string,
elm: Element = vnode.elm as Element,
oldAttrs = (oldVnode.data as VNodeData).attrs,
attrs = (vnode.data as VNodeData).attrs;
if (!oldAttrs && !attrs) return;
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, "");
} else if (cur === false) {
elm.removeAttribute(key);
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur);
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur);
} else {
elm.setAttribute(key, cur);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
export const attrsModule = {
create: updateAttrs,
update: updateAttrs
} as Module;
//------------------------------------------------------------------------------
// class.ts
//------------------------------------------------------------------------------
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any,
name: string,
elm: Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
export const classModule = { create: updateClass, update: updateClass } as Module;

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