Compare commits

..

124 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
Géry Debongnie c3453d35b9 [IMP] hooks/component: remove updateEnv, add useSubEnv
closes #182
2019-10-04 22:54:27 +02:00
Géry Debongnie b0dd0252a4 [REF] component: move method at proper place 2019-10-04 22:54:27 +02:00
Géry Debongnie e2b2e54e29 [DOC] tooling: add missing link 2019-10-04 22:12:56 +02:00
Géry Debongnie 766c378ac9 [DOC] hooks: add missing doc for onWillPatch/onPatched 2019-10-03 11:57:58 +02:00
Géry Debongnie 022b29b6a0 [IMP] component/qweb: remove t-mounted directive
The t-mounted directive main goals can be achieved with hooks, in a
better and more intuitive way.

closes #308
2019-10-03 10:57:24 +02:00
Géry Debongnie 367725d194 [IMP] hooks: add onWillPatch and onPatched
This commit also changes the behaviour of willPatch and patched: they no
longer transfer data from one to the other, because it can be done
cleanly by hooks and a closure.

closes #307
2019-10-03 10:20:28 +02:00
Géry Debongnie e82ed7dc8c [DOC] component: add information on svg components 2019-10-02 16:40:13 +02:00
Géry Debongnie 63c3d4f6b0 [IMP] playground: slightly improve default text 2019-10-01 21:37:36 +02:00
Géry Debongnie b859fcb8f0 [REL] bump to v0.22.0 2019-10-01 21:05:04 +02:00
Géry Debongnie 3005756d89 [LEGAL] add copyright file 2019-10-01 20:44:39 +02:00
Géry Debongnie f6820bb8ac [REF] hooks: better implementation of onMounted/onWillUnmount 2019-09-28 21:14:40 +02:00
Géry Debongnie 090880f478 [DOC] readme: add quick overview section 2019-09-27 17:34:27 +02:00
Géry Debongnie 5cdaa7b473 [IMP] hooks: implement useRef hook
closes #194
2019-09-27 11:20:08 +02:00
Géry Debongnie 91b9e78182 [DOC] update component, tooling doc 2019-09-27 11:20:08 +02:00
Géry Debongnie 5335fb8fba [IMP] components: add hooks mechanism
part of #194
2019-09-27 11:20:08 +02:00
Géry Debongnie 4513e3f31c [FIX] qweb: improved svg support for components
If we want to generate dynamically svg components, it is useful to be
able to define components with a <g> tag as root, and then getting the
correct namespace.

closes #302
2019-09-26 15:46:42 +02:00
Géry Debongnie ecf12f6eee [FIX] qweb: properly supports svg
Snabbdom supports svg, but it adds the namespace at the creation of the
virtual node.  However, owl works slightly differently: it adds children
after creating the parent virtual node.

So, we need to actually call the addNS method after the children nodes
have been created.

As a bonus, this is slightly faster than snabbdom: we only check at
template compilation time once if a node is a svg.
2019-09-24 21:40:46 +02:00
Géry Debongnie 82a6961b3a [FIX] component: properly handle wrapper component and slots
closes #297
2019-09-23 07:13:51 +02:00
Géry Debongnie 78a4550ebb [IMP] component tags: allow injecting values in xml tag string
With this commit, we can now do this:

      const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`
      class Parent extends Widget {
          static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`
          state = {n: 42};
      }
2019-09-21 23:05:44 +02:00
Géry Debongnie cc82b3ffcd [IMP] component: support dynamic t-component 2019-09-21 21:58:19 +02:00
Géry Debongnie bbbaf95c8e Merge pull request #296 from odoo/issue-293-owl-fibers
[MERGE] component: introduce owl fiber
2019-09-20 13:54:59 +02:00
Géry Debongnie caa63628dc [REF] component: refactor fiber creation methods 2019-09-20 11:14:50 +02:00
Géry Debongnie 7a49b9d94e [REF] component: move render props and promise to fiber
part of #293
2019-09-20 10:48:10 +02:00
Géry Debongnie 01eb338e69 [REF] component: move patchqueue management to fiber
part of #293
2019-09-20 10:47:58 +02:00
Géry Debongnie 3048c6f961 [REF] component: introduce fiber
part of #293
2019-09-20 10:47:55 +02:00
Géry Debongnie 8edc637033 [FIX] qweb: remove comments in xml strings before parsing
It was done in loadTemplates, but this does not work if we want to add
templates from another source.
2019-09-19 15:18:59 +02:00
Géry Debongnie c9910077a4 [FIX] qweb: do not capture current qweb instance in closure
it is not needed, and has a bad effect on globally registered templates.
2019-09-12 23:23:55 +02:00
Géry Debongnie 6bf70f016c [DOC] doc: update readme.md 2019-09-12 23:23:39 +02:00
Géry Debongnie 905619f80f [ADD] tools: add benchmark for 0.21.0 2019-09-12 14:50:56 +02:00
Géry Debongnie 14ceb382ef [REL] bump to v0.21.0 2019-09-12 14:21:36 +02:00
Géry Debongnie 9846b2e997 [IMP] tags: introduce xml tag
Big change! This commit introduces an xml function tag to easily define
inline templates.

This is a pretty big change toward single file owl components

Part of #284
2019-09-12 14:17:21 +02:00
Géry Debongnie cfc2fb2ba2 [REF] qweb: rename register method to registerComponent 2019-09-12 14:17:21 +02:00
Géry Debongnie d57017b6ed [IMP] qweb: add registerTemplate method 2019-09-12 14:17:21 +02:00
Géry Debongnie 5bf7a5c671 [IMP] component: make template key static
closes #288
2019-09-12 14:17:21 +02:00
Géry Debongnie 9b589af75a [REF] qweb: improve generated code in some cases 2019-09-12 09:43:09 +02:00
Géry Debongnie 471fd49063 [IMP] router: replace t-routecomponent directive by RouteComponent
closes #287
2019-09-11 14:58:36 +02:00
Géry Debongnie 4fc3423682 [IMP] component: support dynamic t-props
It is useful in some rare situations.

closes #144
2019-09-11 14:12:52 +02:00
Géry Debongnie ffb3263c79 [IMP] component: allow dynamic root nodes
closes #283
2019-09-11 13:12:57 +02:00
Géry Debongnie e1acf66143 [IMP] component: components key is now static
This is a breaking change!

closes #279
2019-09-11 11:18:49 +02:00
Géry Debongnie 2a40907e35 [FIX] qweb: do not crash in some rare t-call situation
Before this commit, QWeb crashed when it had to deal with a t-call with
multiple sub nodes on the same line, and one text node:

<t t-call="SomeTemplate">
  <span>hey</span> <span>hey</span>
</t>

This was because we need to compile the content to be able to extract
all possible t-set t-value statements. But doing so means that we had a
multiple root templates, which caused a crash in this case.

Solving this issue is simple: the sub template is compiled with the flag
allowMultipleRoots set to true.
2019-09-09 11:51:33 +02:00
Géry Debongnie 1c0dd79955 [MISC] update package.json 2019-09-05 21:44:29 +02:00
Géry Debongnie 9fad164c79 [REL] bump to v0.20.0 2019-09-05 14:41:12 +02:00
Géry Debongnie 53a8841914 [TEST] component: snapshots slot functions 2019-09-05 14:33:43 +02:00
Géry Debongnie 19f0863aa3 [IMP] qweb: allow recursive templates t-calls
closes #267
2019-09-05 14:33:43 +02:00
Géry Debongnie 28f7b44f9e [IMP] component: make t-model works with sub state keys
closes #273
2019-09-05 08:47:37 +02:00
Géry Debongnie fe46a6ccee [CLEANUP] run prettier on some code 2019-09-04 22:48:37 +02:00
Géry Debongnie 3c23a7b246 [FIX] component: t-model properly works with select
The initial value was not set on the select tag.

closes #271
2019-09-04 22:45:21 +02:00
Géry Debongnie c52799de2c [FIX] ConnectedComponent: keep props uptodate
This is a tricky bug. The problem is that a ConnectedComponent can
trigger a rendering before it is aware that there is a state change in
the store, and just before the store update event comes in.

What we do in this commit is to update the storeProps whenever a
rendering is scheduled.  However, we need to keep the rendering
information as a promise to give it in some case to the __checkUpdate
method (see the note in the render method)

closes #268
2019-09-04 15:36:10 +02:00
Géry Debongnie 4edb90f44b [FIX] router: properly handle popstate
It was not correct, the url was changed multiple times and back
navigation was broken.

Also, note that there are no tests because the history api is not
available in jsdom.
2019-09-04 13:59:54 +02:00
Géry Debongnie 0c1a7aabae [FIX] component: better templateId for multiple subcomponents
Of course, the templateId computation is kind of tricky.  Here, an issue
occurred because the templateId did not take the componentId into
account when we were in a loop, but with no keyed parent.

This meant that multiple sub components shared the same templateId,
confusing the vdom algorithm.
2019-09-03 16:38:24 +02:00
Géry Debongnie cfdce29ce7 [FIX] component: avoid key collisions in sub components
This is a difficult part of owl: we need to be able to reconcile the
vdom generated (this is done with our virtual dom algorithm), but also
to be able to reconcile the proper components.

The issue here is that when we are in a list, with a t-key attribute on all
list nodes, and those list nodes contains sub component, then the
component system used the index of the list, and did not take into
account the key from the parent.  The fix is to keep track of the
current key in the context, and uses that as a part of the templateId.
2019-09-03 12:13:57 +02:00
Géry Debongnie b344d73e07 [FIX] component: prevent collision between template ids and keys 2019-09-02 15:27:37 +02:00
Géry Debongnie 4cd08d25c8 [FIX] component: solve tricky concurrency issue
In some specific situation, a component could crash, because it was
destroyed before being mounted, but reused anyway for another rendering.
2019-09-01 21:24:54 +02:00
Géry Debongnie f9861040c8 [FIX] router: properly react on hashchange
Before this commit, hashchanges were not taken into account by the
router, if used in history mode.

Also, it's stupid, but i ran prettier on the codebase
2019-09-01 11:45:13 +02:00
Géry Debongnie 40d8090232 [FIX] router: update app if parameterized route changes 2019-08-31 15:03:17 +02:00
Géry Debongnie 55be6437f1 [FIX] router: better support for hash mode 2019-08-31 09:55:45 +02:00
Géry Debongnie 6ff4dd74bf [DOC] readme: improve main example 2019-08-28 12:22:32 +02:00
Géry Debongnie 3628b49c37 [DOC] update readme (swap some sections around) 2019-08-28 12:05:53 +02:00
Géry Debongnie f289cb17ec [REL] bump to v0.19.0 2019-08-28 11:55:36 +02:00
Géry Debongnie e6a3ede4a6 [FIX] component: improve unmount/remount behaviour 2019-08-28 11:41:10 +02:00
Alexandre Kühn 10cfe0b740 [IMP] store: dispatch method returns data from action 2019-08-27 22:26:16 +02:00
Géry Debongnie a3d563ebb4 [FIX] connected_component: some misc issues
- did not properly stop listening to the store after being destroyed
- did not properly synchronize store updates in some async cases
2019-08-27 17:11:54 +02:00
Géry Debongnie 3b1247d476 [REF] vdom: small cleanup 2019-08-27 16:01:01 +02:00
Géry Debongnie beb6c2bc82 [DOC] improve readme and comparison
closes #244
2019-08-27 14:48:02 +02:00
Géry Debongnie f0cd6f61cd [FIX] playground: fix crash in todo app example 2019-08-27 14:09:20 +02:00
Géry Debongnie c950b385a0 [ADD] playground: add window management system example
closes #231
2019-08-27 14:09:20 +02:00
Géry Debongnie c172b31481 [DOC] router: add documentation 2019-08-26 15:45:55 +02:00
Géry Debongnie 9b3a97e732 [DOC] component: improve internal documentation 2019-08-26 12:19:11 +02:00
Géry Debongnie 670c98916c [DOC] store: update documentation and add examples 2019-08-26 10:31:11 +02:00
Géry Debongnie c59abef374 [IMP] connected component: add dispatch method 2019-08-26 10:31:11 +02:00
Géry Debongnie bb4c2506c0 [REF] store: merge actions and mutations
closes #256
2019-08-26 10:31:11 +02:00
Géry Debongnie 9843d16585 [REF] component: improve render method 2019-08-26 10:31:11 +02:00
Géry Debongnie 34e463ad0a [REF] connected component: separate storeprops from props
closes #259
2019-08-26 10:31:11 +02:00
Géry Debongnie 2294c1967f [IMP] qweb: allow xml documents as template definition
Before this commit, the addTemplates method only accepts a string.  But
whenever we perform a $.get call, we get a parsed xml document. So, to
make Owl more convenient to use in that situation, we simply accept xml
documents in addition to strings.
2019-08-23 15:17:28 +02:00
Géry Debongnie 4b53e48e91 [ADD] tools: add benchmarks for 0.18.0 2019-08-22 15:26:17 +02:00
Géry Debongnie 70a70161e7 [REL] bump to v0.18.0 2019-08-22 14:56:39 +02:00
Géry Debongnie e690991a6a [FIX] component: better heuristic to determine if was mounted 2019-08-22 14:55:31 +02:00
Géry Debongnie a621a14028 [MERGE] router: add owl router
First implementation of a declarative router component/directive/plugin
2019-08-22 14:49:11 +02:00
Géry Debongnie 9754cc7521 [REF] small cleanup 2019-08-22 14:26:34 +02:00
Géry Debongnie aa268937a7 [IMP] router: navigate now returns more information 2019-08-22 14:26:34 +02:00
Géry Debongnie 3db6f0a643 [IMP] owl: add commonjs build script 2019-08-22 14:26:34 +02:00
Géry Debongnie 635a7fc72c [FIX] router: do not prevent default click on <a> in some cases 2019-08-22 14:26:34 +02:00
Géry Debongnie 6cd5ec10f0 [FIX] router: properly redirect if necessary when started 2019-08-22 14:26:34 +02:00
Géry Debongnie 0a276fbf1d [IMP] router: basic implementation of navigation guards 2019-08-22 14:26:34 +02:00
Géry Debongnie d593131cc1 [REF] router: remove useless check 2019-08-22 14:26:34 +02:00
Géry Debongnie 3c3dcb44a9 [IMP] router: add test for Link 2019-08-22 14:26:34 +02:00
Géry Debongnie 6ecd1d6ea2 [IMP] router: add validation for destinations 2019-08-22 14:26:34 +02:00
Géry Debongnie 1a931a2a08 [IMP] router: implement redirect feature 2019-08-22 14:26:34 +02:00
Géry Debongnie bf705fcd99 [REF] router: add TestRouter and improve tests 2019-08-22 14:26:34 +02:00
Géry Debongnie bb46346767 [REF] router: change Destination shape 2019-08-22 14:26:34 +02:00
Géry Debongnie cd0762b436 [REF] rework router into a class 2019-08-22 14:26:34 +02:00
Géry Debongnie ca920a0091 [ADD] router: initial implementation 2019-08-22 14:26:34 +02:00
Géry Debongnie e5b1ba24d8 [IMP] component: allow multiple mount/unmount
closes #258
2019-08-22 14:23:08 +02:00
Géry Debongnie eaf912bed7 [FIX] observer: properly handle dates 2019-08-22 14:22:20 +02:00
Géry Debongnie fd0fb5be00 [REF] observer: refactor observer using proxy
closes #253
2019-08-22 14:22:20 +02:00
Géry Debongnie f9f730ab7b [DOC] add some info about return value from actions 2019-08-22 13:05:54 +02:00
Géry Debongnie b30b72fc36 [FIX] update benchmark application 2019-08-22 13:05:37 +02:00
Alexandre Kühn ab08e84bff [IMP] store: return data from dispatching an action 2019-08-22 13:04:32 +02:00
Géry Debongnie cd9cac1246 [REF] qweb: add forceUpdate method
and make sure it only triggers an update once for multiple calls in same
call stack
2019-07-30 15:00:51 +02:00
Géry Debongnie f6369f9750 [REF] qweb: put directives in static QWeb prop
so they can be altered by outside code.  This is mostly useful for tests.
2019-07-30 14:37:39 +02:00
Géry Debongnie f18c73e59f [FIX] component: solve a tricky concurrency rendering issue
When components were initially rendered more than once, in consecutive
microtask ticks, the order of two actions was reversed: the assignation
of the vnode to the __owl__.vnode property, and the assignation of
__owl__.vnode to the final rendered vdom of the component.

As a result, the widget was rendered as null (so, not present).

The reason for this issue was that the renderPromise key was reassigned
at some point.
2019-07-30 08:54:55 +02:00
Géry Debongnie 93d0a7dd46 [FIX] store: properly handle defaultProps in connected components
Thanks to aku for issue + tests + first solution

closes #245
2019-07-16 20:51:57 +02:00
Géry Debongnie b1d022b583 [FIX] qweb: allow t-if with t-call directives
closes #246
2019-07-16 13:19:21 +02:00
Géry Debongnie 5382e824b1 [REF] owl: reorganize src code in sub files 2019-07-16 13:12:08 +02:00
Géry Debongnie 15a3f59744 [REF] owl: move some exported values around
This is a breaking change. Code using Observer, EventBus, Store or
ConnectedComponent need to update the way they import theses classes.

- Observer is now in owl.misc (so you can import it like this:
    const Observer = owl.misc.Observer;)
- EventBus is also now in owl.misc
- Store and ConnectedComponent are now together in store
2019-07-14 09:02:02 +02:00
Géry Debongnie 4a90ec7ff0 [ADD] tools: add v0.17.0 benchmark 2019-07-13 17:13:18 +02:00
Géry Debongnie 9f9b6b174e [REL] bump to v0.17.0 2019-07-13 17:06:43 +02:00
Géry Debongnie 545ceefc3d [REF] store: remove connect function, add ConnectedComponent
closes #238
closes #235
2019-07-13 17:04:36 +02:00
Géry Debongnie 591508e769 [IMP] component: allow top level widgets
closes #166
2019-07-12 14:31:34 +02:00
Géry Debongnie d9cbd23cd9 [IMP] component: add catchError hook 2019-07-12 09:52:09 +02:00
Géry Debongnie f4cd111cc8 [IMP] qweb: more information in an error message 2019-07-11 16:22:30 +02:00
Géry Debongnie ea015af741 [FIX] component: in slots, qweb was looking in wrong component
In slots, the actual parent is where the system should look for existing
widgets, not the rendering context.

closes #239
2019-07-11 15:00:05 +02:00
Géry Debongnie bfc7c81c0d [FIX] component: slots should preserve parented relation
closes #234
2019-07-10 11:01:15 +02:00
Géry Debongnie e24ff8f8aa [IMP] qweb: add renderToString method
closes #230
2019-07-09 10:16:27 +02:00
Géry Debongnie 9f1e64d399 [FIX] component: issue with t-slot,t-set,t-value, but no t-foreach
closes #232
2019-07-08 17:04:06 +02:00
Géry Debongnie 8e1aa71436 [FIX] component: scope issue with slots
Slot templates need to be able to access variables from the parent
scope.

closes #228
2019-07-08 16:24:57 +02:00
Géry Debongnie be2fc965b1 [IMP] playground: reduce duplication in responsive sample 2019-07-08 14:16:12 +02:00
Géry Debongnie 7245ccf8f9 [IMP] component: various prop validation improvements
- add optional form for list props: ['optionalField?']
- accept undefined values for optional props
- allow declaring props with only boolean true
- throw error if extra prop is given to component

closes #223
2019-06-28 16:53:14 +02:00
Géry Debongnie 1c8e1af86d [IMP] tools: improve error message display 2019-06-28 14:54:24 +02:00
Mathieu Duckerts-Antoine 60a6cca960 [IMP] qweb: better error messages for bad xml templates
closes #185
2019-06-28 14:53:19 +02:00
Géry Debongnie ad55b42ccb [IMP] playground: improve first example
Previous example did not illustrate props
2019-06-28 14:31:19 +02:00
Géry Debongnie 50c0a4b126 [DOC] improve component documentation 2019-06-28 13:00:38 +02:00
Géry Debongnie 2ca42e7470 [ADD] tools: add v0.16.0 benchmark 2019-06-28 11:51:37 +02:00
93 changed files with 28433 additions and 6146 deletions
+14
View File
@@ -0,0 +1,14 @@
Most of the files are
Copyright (c) 2004-2015 Odoo S.A.
Many files also contain contributions from third
parties. In this case the original copyright of
the contributions can be traced through the
history of the source version control system.
When that is not the case, the files contain a prominent
notice stating the original copyright and applicable
license, or come with their own dedicated COPYRIGHT
and/or LICENSE file.
+224 -60
View File
@@ -1,19 +1,71 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
_A web framework for structured, dynamic and maintainable applications_
_A no nonsense web framework for structured, dynamic and maintainable applications_
## Project Overview
The Odoo Web Library (OWL) is a small
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
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:
**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.
- a declarative component system,
- a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router
## OWL's Design Principles
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
**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
import { Component, QWeb, useState } from "owl";
import { xml } from "owl/tags";
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++;
}
}
class App extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter };
}
const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
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, code needs to be maintained by large teams.
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
@@ -26,70 +78,182 @@ requirements are common, code needs to be maintained by large teams.
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 or small (even though it is quite good on those
two topics). If you are interested in a comparison with React or Vue, you will
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).
## Example
Here is a short example to illustrate interactive components:
```xml
<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
```javascript
class Counter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
const qweb = new owl.QWeb(TEMPLATES);
const counter = new Counter({ qweb });
counter.mount(document.body);
```
Note that we assume here that the xml templates are available in the `TEMPLATES`
string. More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.16.0.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.js)
- [owl-0.16.0.min.js](https://github.com/odoo/owl/releases/download/v0.16.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 tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
- [Hooks](doc/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [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 [GPL licensed](./LICENSE).
OWL is [LGPL licensed](./LICENSE).
+53 -13
View File
@@ -10,6 +10,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
## Content
- [Size](#size)
- [Class Based](#class-based)
- [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering)
@@ -21,12 +22,23 @@ discussed, feel free to open an issue/submit a PR to correct this text.
OWL is intended to be small and to work at a slightly lower level of abstraction
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
| Framework | Size (minified) | Size (minified, gzipped) |
| ------------------------ | --------------- | ------------------------ |
| OWL | 32kb | 11kb |
| Vue + VueX | | 30kb |
| React + ReactDOM + Redux | | 40kb |
| jQuery | 86kb | 30kb |
| Framework | Size (minified, gzipped) |
| ------------------------ | ------------------------ |
| OWL | 16kb |
| Vue + VueX | 30kb |
| React + ReactDOM + Redux | 40kb |
| jQuery | 30kb |
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
a more functional approach, in particular, with the new `hooks` mechanisms.
This has some advantages and disadvantages. But the end result is that React
and Vue both offers multiple different ways of defining new components. In
contrast, Owl has only one mechanism: class-based components. We believe that Owl
components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
## Tooling/Build step
@@ -45,7 +57,7 @@ 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.
### Templating
## Templating
OWL uses its own QWeb engine, which compiles templates on the
frontend, as they are needed. This is extremely convenient for our use case, in
@@ -128,8 +140,10 @@ by getters/setters. With that, it can notify components whenever the state that
they read was changed.
Owl is closer to vue: it also tracks magically the state properties, but it does
only increment a counter whenever it changes (and a _deep_ counter for each of
its parents). This assumes that the state is actually a tree.
only increment an internal counter whenever it changes. Note that it is done
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).
## State Management
@@ -190,7 +204,33 @@ keeps track of who get data, and retrigger a render when it was changed.
**Owl**
Owl store is a little bit like a mix of redux and vuex: it has mutations and
actions, like VueX, it keeps track of the state changes, but it does not notify
a component when the state changes. Instead, components need to connect to the
store like in redux, with a function that will listen to the relevant state.
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.env.store.dispatch("increment");
}
}
const counter = new Counter({ store, qweb });
```
+354 -124
View File
@@ -5,6 +5,7 @@
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Reactive System](#reactive-system)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
@@ -15,12 +16,14 @@
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [`t-mounted` Directive](#t-mounted-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
## Overview
@@ -44,16 +47,15 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
## Example
Let us have a look at a simple component:
```javascript
const { useState } = owl.hooks;
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -71,11 +73,10 @@ Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
Component class. If no `template` key is defined, then
Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
[observed](observer.md), and any change to it will cause a rerendering.
## Reference
@@ -88,6 +89,25 @@ Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor).
### Reactive system
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
The main idea is that the `useState` hook generate a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change.
```javascript
const { useState } = owl.hooks;
class SomeComponent extends owl.Component {
state = useState({ a: 0, b: 1 });
}
```
Note that there is an important limitation: hooks need to be called in the
constructor.
### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
@@ -95,40 +115,67 @@ find a template with the component name (or one of its ancestor).
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render
the component.
- **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each
change will cause the component to rerender itself.
- **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties
- **`props`** (Object, optional): if given, this is an object that describes the
- **`template`** (string, optional): if given, this is the name of the QWeb 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. This is the main way
used by Owl to be able to create sub components.
```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-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.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0
};
}
```
### Methods
We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
DOM: the root component is mounted to a target HTMLElement. Obviously, this
is asynchronous, since each children need to be created as well. Most applications
will need to call `mount` exactly once, on the root component.
- **`mount(target, renderBeforeRemount=false)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement. Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `renderBeforeRemount` argument is useful when a component is unmounted and remounted.
In that case, we may want to rerender the component _before_ it is remounted, if
we know that its state (or something in the environment, or ...) has changed.
In that case, it should simply set to `true`.
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
@@ -160,7 +207,7 @@ We explain here all the public methods of the `Component` class.
framework instead.
Obviously, these methods are reserved for Owl, and should not be used by Owl
users, unless they want to override them. Also, Owl reserves all method names
users, unless they want to override them. Also, Owl reserves all method names
starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change.
@@ -170,15 +217,16 @@ A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of
a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------- |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| Method | Description |
| ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
Notes:
@@ -200,7 +248,7 @@ component.
```javascript
constructor(parent, props) {
super(parent, props);
this.state = {someValue: true};
this.state = useState({someValue: true});
this.template = 'mytemplate';
}
```
@@ -210,7 +258,7 @@ implemented in most cases:
```javascript
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
...
}
@@ -235,8 +283,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as
fast as possible.
The component rendering will take place after `willStart` is completed.
#### `mounted()`
`mounted` is called each time a component is attached to the
@@ -250,10 +296,9 @@ always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First,
the parent, then all its children.
Note that the state is now observed. It is however 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.
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.
#### `willUpdateProps(nextProps)`
@@ -278,14 +323,11 @@ It is not called on the initial render. This is useful to read some
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state object is not allowed here. This method is called just
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 (this can
happen with components with `t-keepalive`).
The return value of this method will be given as the first argument of the
corresponding `patched` call.
#### `patched(snapshot)`
This hook is called whenever a component did actually update its DOM (most
@@ -296,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
The `snapshot` parameter is the result of the previous `willPatch` call.
Updating the compoent 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 patched. So, we need to be particularly
@@ -319,6 +359,12 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling)
### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
@@ -353,22 +399,22 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment
could be programmed like this:
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to call the
`forceUpdate` method on the QWeb instance. For example, a responsive environment
could be done like this:
```js
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger('update');
}
}, 15);
window.addEventListener("resize", updateEnv);
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
}
```
@@ -387,7 +433,7 @@ to be capitalized.
```js
class ParentComponent extends owl.Component {
components = { MyComponent: MyComponent};
static components = { MyComponent: MyComponent};
...
}
```
@@ -399,7 +445,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component.
that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml
<div t-name="ParentComponent">
@@ -409,16 +456,16 @@ that the template can access `state`, `props`, `env`, or any methods defined in
```js
class ParentComponent {
components = { ChildComponent };
state = { val: 4 };
static components = { ChildComponent };
state = useState({ val: 4 });
}
```
Whenever the template is rendered, it will automatically create the subcomponent
`ChildComponent` at the correct place. It needs to find the reference to the
actual component class in the special `components` key, or the class registered in
actual component class in the special static `components` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `components` key, then fallbacks on the global registry.
the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
@@ -428,7 +475,8 @@ the sub component will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
@@ -438,11 +486,51 @@ The `t-component` directive can also be used to accept dynamic values with strin
```js
class ParentComponent {
components = { ChildComponent1, ChildComponent2 };
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
And 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 } };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element.
@@ -558,7 +646,7 @@ form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = { text: "" };
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
@@ -578,8 +666,9 @@ 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
is the (top-level) name in the state object. With the `t-model` directive, we
can write a shorter code, equivalent to the previous example:
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 {
@@ -589,7 +678,7 @@ class Form extends owl.Component {
```xml
<div>
<input t-model="text" />
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
@@ -599,11 +688,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
```xml
<div>
<div>Text in an input: <input t-model="someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></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="color">
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
@@ -612,11 +701,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="color"/>
<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="color" />
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
@@ -634,7 +723,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example:
```xml
<input t-model.lazy="someVal" />
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -666,24 +755,6 @@ There are three main use cases:
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-mounted` Directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyComponent extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Semantics
We give here an informal description of the way components are created/updated
@@ -786,7 +857,8 @@ of the props. Here is how it works in Owl:
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
- if a key does not match the description, an error is thrown
- it only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated.
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example:
@@ -814,15 +886,16 @@ class ComponentB extends owl.Component {
- 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.
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, validation 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 constructor, a list of constructors, or an object:
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
- a list of constructors. In that case, this means that we allow more than one
@@ -840,6 +913,11 @@ Examples:
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
@@ -854,50 +932,67 @@ Examples:
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
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
};
```
### References
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
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. 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>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someComponent;
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub component.
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, if we have
`id` set to 44 in the rendering context,
The `useRef` hook can also be used to get a reference to an instance of a sub
component rendered by Owl. In that case, we need to access it with the `comp`
property instead of `el`:
```xml
<div t-ref="component_{{id}}"/>
<div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
```
```js
this.refs.component_44;
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
static components = { SubComponent };
subRef = useRef("sub");
doSomething() {
this.subRef.comp.doSomeThingElse();
}
}
```
Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Slots
To make generic components, it is useful to be able for a parent component to _inject_
@@ -994,3 +1089,138 @@ Here are a few tips on how to work with asynchronous components:
<AsyncChild t-asyncroot="1"/>
</div>
```
### Error Handling
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 state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></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, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component {
static template = xml`
<div>
<t t-call=${Welcome}/>
<div>something</div>
</div>
`;
}
```
The way this works is that sub templates are inlined, and have access to the
ambient context. They can therefore access `props`, and any other part of the
caller component.
### SVG Components
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.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
+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.
+3 -33
View File
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
observer.observe(obj);
const obj = observer.observe({ a: { b: 1 } });
const obj = { a: { b: 1 } };
obj.a.b = 2;
```
## Technical Limitations
Since the observer uses getters and setters, it is actually unable to react to
changes in three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
+4 -2
View File
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
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.template = "clickcounter";
this.state = { value: 0 };
this.state = useState({ value: 0 });
}
increment() {
+28 -13
View File
@@ -65,16 +65,15 @@ We present here a list of all standard QWeb directives:
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-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-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
| 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
@@ -101,7 +100,7 @@ It's API is quite simple:
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.
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).
@@ -122,14 +121,30 @@ It's API is quite simple:
const vnode = qweb.render("App", component);
```
- **`register(name, Component)`**: static function to register an OWL 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.register("Dialog", Dialog);
QWeb.registerComponent("Dialog", Dialog);
...
+44
View File
@@ -1,13 +1,56 @@
# 🦉 OWL Documentation 🦉
## Owl Content
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
```
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
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
- [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)
@@ -19,3 +62,4 @@
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
+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>
```
+116 -85
View File
@@ -5,8 +5,7 @@
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Public API](#public-api)
- [Mutations](#mutations)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
@@ -22,11 +21,10 @@ 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 (through
mutations and actions). Owl components can then connect to the store, and will
be updated if necessary.
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's store is inspired by React Redux and VueX.
Note: Owl store is inspired by React Redux and VueX.
## Example
@@ -34,19 +32,12 @@ Here is what a simple store looks like:
```js
const actions = {
addTodo({ commit }, message) {
commit("addTodo", message);
}
};
const mutations = {
addTodo({ state }, message) {
const todo = {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false
};
state.todos.push(todo);
});
}
};
@@ -55,66 +46,65 @@ const state = {
nextId: 1
};
const store = new owl.Store({ state, actions, mutations });
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
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
### `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. This implies that
state changes need to be done carefully in some cases (adding a new key to an
object, or modifying an array with the `arr[i] = newValue` syntax). See the
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
### Public API
The `Store` class is quite small. It has two public methods:
1. `constructor`
2. `commit`
3. `dispatch`
- its constructor
- `dispatch`
### Mutations
The constructor takes a configuration object with four (optional) keys:
Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous.
- the initial state
- the actions
- the getters
- the environment
```js
const mutations = {
setLoginState({ state }, loginState) {
state.loginState = loginState;
}
```javascript
const config = {
state,
actions,
getters,
env
};
```
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
```js
store.commit("setLoginState", "error");
const store = new Store(config);
```
### Actions
Actions are used to coordinate state changes. It is also useful whenever some
asynchronous logic is necessary. For example, fetching data should be done
in an action.
Actions are used to coordinate state changes. It can be used for both synchronous
and asynchronous logic.
```js
const actions = {
async login({ commit }, info) {
commit("setLoginState", "pending");
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
commit("setLoginState", loginInfo);
} catch {
commit("setLoginState", "error");
state.loginState = loginInfo;
} catch (e) {
state.loginState = "error";
}
}
};
@@ -127,6 +117,39 @@ arbitrary number of arguments.
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
@@ -165,47 +188,41 @@ const getters = {
const post = store.getters.getPost(id);
```
Getters take *at most* one argument.
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
By default, an Owl `Component` is not connected to any store. The `connect`
function is there to create sub Components that are connected versions of
Components.
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({ commit }) {
commit("increment", 1);
}
};
const mutations = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions, mutations });
const store = new owl.Store({ state, actions });
class Counter extends owl.Component {
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb });
const counter = new Counter({ store, qweb });
```
```xml
@@ -214,45 +231,59 @@ const counter = new ConnectedCounter({ store, qweb });
</button>
```
The arguments of `connect` are:
The `ConnectedComponent` class can be configured with the following fields:
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict
- `options`: dictionary of optional parameters that may contain
- `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`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)
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)
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
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 `connect` function try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
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
- 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
- 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.
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>
`;
...
}
```
+32
View File
@@ -6,6 +6,7 @@
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
## Overview
@@ -37,6 +38,9 @@ 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
@@ -53,3 +57,31 @@ 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...
}
```
+1 -3
View File
@@ -18,9 +18,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
templates
]) {
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
+8 -7
View File
@@ -1,12 +1,13 @@
{
"name": "owl",
"version": "0.16.0",
"name": "owl-framework",
"version": "0.22.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
"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:watch": "jest --watch",
@@ -20,16 +21,18 @@
"url": "git+https://github.com/odoo/owl.git"
},
"author": "Odoo",
"license": "GPL-3.0-or-later",
"license": "LGPL",
"bugs": {
"url": "https://github.com/odoo/owl/issues"
},
"homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": {
"@types/jest": "^23.3.12",
"cpx": "^1.5.0",
"git-rev-sync": "^1.12.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"rollup": "^1.6.0",
@@ -38,10 +41,8 @@
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2",
"uglify-es": "^3.3.9",
"jest-environment-jsdom": "^24.7.1"
"uglify-es": "^3.3.9"
},
"dependencies": {},
"jest": {
"roots": [
"<rootDir>/src",
@@ -62,6 +63,6 @@
]
},
"prettier": {
"printWidth": 100
"printWidth": 100
}
}
+285 -234
View File
@@ -1,6 +1,8 @@
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom";
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
@@ -9,7 +11,8 @@ import { h, patch, VNode } from "./vdom";
* contains:
*
* - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component)
* - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
@@ -28,6 +31,28 @@ import { h, patch, VNode } from "./vdom";
*/
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;
}
/**
@@ -35,41 +60,49 @@ export interface Env {
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
export interface Meta<T extends Env, Props> {
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: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to componentID
// should it be a map number => Component?
// 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 };
renderId: number;
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any };
observer?: Observer;
render?: CompiledTemplate;
mountedHandlers: { [key: number]: Function };
classObj?: { [key: string]: boolean };
}
currentFiber: Fiber<Props> | null;
// If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by
// qweb instances.
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
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 {}, State extends {}> {
readonly __owl__: Meta<Env, Props>;
template?: string;
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:
@@ -80,17 +113,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
env: T;
state?: State;
props: Props;
// type of props is not easily representable in typescript...
static props?: any;
static defaultProps?: any;
refs: {
[key: string]: Component<T, any, any> | HTMLElement | undefined;
} = {};
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
@@ -114,27 +138,30 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
this.__validateProps(props || {});
}
// 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, any> | null = null;
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);
@@ -157,11 +184,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
currentFiber: null,
boundHandlers: {},
mountedHandlers: {}
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
observer: null,
render: null,
classObj: null,
refs: null
};
}
@@ -208,8 +240,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 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
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
@@ -225,10 +255,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 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.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
@@ -241,6 +269,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -250,14 +284,29 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*
* 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): Promise<void> {
const vnode = await this.__prepare();
if (this.__owl__.isDestroyed) {
// component was destroyed before we get here...
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return;
}
this.__patch(vnode);
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)) {
@@ -265,6 +314,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
/**
* 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();
@@ -272,23 +325,29 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
/**
* 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 shouldPatch: boolean = !patchQueue;
if (shouldPatch) {
patchQueue = [];
}
const renderVDom = this.__render(force, patchQueue);
const renderId = __owl__.renderId;
await renderVDom;
const fiber = this.__createFiber(force, undefined, undefined, undefined);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
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(<any[]>patchQueue);
this.__applyPatchQueue(fiber);
}
}
@@ -321,26 +380,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return true;
}
/**
* This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
* static config keys, such as a boolean to determine if we are in mobile
* mode or not.
*/
async updateEnv(nextEnv: Partial<T>): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (__owl__.isMounted) {
await this.render(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
@@ -362,7 +401,39 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Private
//--------------------------------------------------------------------------
__destroy(parent: Component<any, any, any> | null) {
/**
* 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) {
@@ -392,16 +463,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB()
}
} catch (e) {
errorHandler(e, this);
}
this.mounted();
}
__callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
@@ -412,97 +489,107 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
/**
* 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,
forceUpdate: boolean = false,
patchQueue?: any[]
parentFiber: Fiber<any>,
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
this.__validateProps(nextProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(forceUpdate, patchQueue);
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__;
__owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel!);
if (this.__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
}
__owl__.vnode = patch(target, vnode);
}
__prepare(): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender();
return __owl__.renderPromise;
/**
* 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(): Promise<VNode> {
await this.willStart();
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;
if (!this.template) {
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
}
let p = (<any>this).constructor;
let name: string = p.name;
let template = tmap[name];
if (template) {
this.template = template;
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 {
tmap[name] = template;
this.template = template;
p._template = template;
}
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState();
return this.__render();
__owl__.render = qweb.render.bind(qweb, p._template);
return this.__render(fiber);
}
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
__render(fiber: Fiber<Props>): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
const patch: any[] = [this];
if (__owl__.isMounted) {
patchQueue.push(patch);
}
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue
});
patch.push(vnode);
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;
}
@@ -513,9 +600,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// parent component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
__owl__.renderProps = this.props;
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
return __owl__.renderPromise;
// 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);
}
/**
@@ -544,15 +637,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
__owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
* Apply default props (only top level).
*
@@ -560,7 +644,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* prop object
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {};
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
@@ -570,103 +654,70 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 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 inverse order
* 3) Call 'patched' on the component of each patch, in reverse order
*/
__applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
/**
* 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.
*/
__validateProps(props: Object) {
const propsDef = (<any>this.constructor).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
if (!(propsDef[i] in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in component '${this.constructor.name}'`
);
__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);
}
}
}
//------------------------------------------------------------------------------
// Prop validation helper
// Error handling
//------------------------------------------------------------------------------
/**
* Check if an invidual prop value matches its (static) prop definition
* 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 isValidProp(prop, propDef): boolean {
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;
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!;
}
// 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);
}
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();
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
@@ -1,186 +1,6 @@
import { QWeb, UTILS } from "./qweb_core";
import { VNode } from "./vdom";
/**
* 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-component/t-keepalive
* - 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
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 }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
UTILS.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
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);
});
};
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({ value, addNodeHook }) {
let name = value;
const hooks = {
insert: `this.utils.transitionInsert(vn, '${name}');`,
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/context";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
@@ -190,6 +10,19 @@ 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
@@ -359,8 +192,10 @@ QWeb.addDirective({
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
@@ -405,32 +240,33 @@ QWeb.addDirective({
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};`);
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
}
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? `String(-${componentID} - i)`
? ctx.currentKey
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
: `String(-${componentID} - i)`
: String(componentID);
if (ctx.allowMultipleRoots) {
// necessary to prevent collisions
if (!key && ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
templateID = `template${id}`;
} else {
templateID = `"_slot_${templateID}"`;
}
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.refs[${refKey}] = w${componentID};`;
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
@@ -438,7 +274,7 @@ QWeb.addDirective({
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`;
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
@@ -471,7 +307,7 @@ QWeb.addDirective({
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `this.utils.toObj(${tattExpr})`;
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -516,23 +352,46 @@ QWeb.addDirective({
}
ctx.addLine(
`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
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(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
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.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
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;`);
@@ -541,23 +400,40 @@ QWeb.addDirective({
ctx.addIf(`!w${componentID}`);
// new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
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.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
`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} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
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
if (node.childNodes.length) {
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++;
const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
@@ -565,8 +441,8 @@ QWeb.addDirective({
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
@@ -574,24 +450,45 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
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=>{${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}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`
`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
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
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}, extra.forceUpdate, ${patchQueueCode});`
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
@@ -600,9 +497,7 @@ QWeb.addDirective({
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}c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
@@ -612,7 +507,7 @@ QWeb.addDirective({
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
@@ -625,103 +520,3 @@ QWeb.addDirective({
return true;
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}}));`
);
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
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 }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
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) => {context.state['${value}'] = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
+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;
}
+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);
}
+19 -12
View File
@@ -4,20 +4,28 @@
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
export { Component } from "./component";
export { EventBus } from "./event_bus";
export { Observer } from "./observer";
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";
// we need to import manually the extra directives so they can register
// themselves in QWeb, otherwise these files will not even be loaded.
import "./qweb_directives";
import "./qweb_extensions";
import { QWeb } from "./qweb_core";
export { Component } from "./component/component";
export { QWeb };
export { connect, Store } from "./store";
import * as _utils from "./utils";
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", {
@@ -36,4 +44,3 @@ Object.defineProperty(__info__, "mode", {
}
}
});
export const utils = _utils;
-194
View File
@@ -1,194 +0,0 @@
/**
* 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 are observed by replacing all their keys with getters/setters
* (recursively)
* - Arrays are observed by replacing their prototype with a customized version,
* which wrap some methods to allow the tracking of each state change.
*
* Note that this code is inspired by Vue.
*/
//------------------------------------------------------------------------------
// Modified Array prototype
//------------------------------------------------------------------------------
// we define here a new modified Array prototype, which basically override all
// Array methods that change some state to be able to track their changes
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto);
for (let i = 0; i < methodLen; i++) {
const method = methodsToPatch[i];
const initialMethod = ArrayProto[method];
ModifiedArrayProto[method] = function(...args) {
if (!this.__observer__.allowMutations) {
throw new Error(`Array cannot be changed here")`);
}
this.__observer__.rev++;
this.__observer__.notifyChange();
this.__owl__.rev++;
let parent = this;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent));
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) {
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
this.__observer__.observe(inserted[i], this);
}
}
return initialMethod.call(this, ...args);
};
}
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
static set(target: any, key: number | string, value: any) {
if (!target.__owl__) {
throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
}
target.__owl__.observer.set(target, key, value);
}
static delete(target: any, key: number | string) {
if (!target.__owl__) {
throw Error("`Observer.delete()` can only be called with observed Objects");
}
target.__owl__.observer.delete(target, key);
}
notifyCB() {}
notifyChange() {
this.dirty = true;
Promise.resolve().then(() => {
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
});
}
observe(value: any, parent?: any) {
if (value === null) {
// fun fact: typeof null === 'object'
return;
}
if (typeof value !== "object") {
return;
}
if ("__owl__" in value) {
// already observed
value.__owl__.parent = parent;
return;
}
if (Array.isArray(value)) {
this._observeArr(value, parent);
} else {
this._observeObj(value, parent);
}
}
set(target: any, key: number | string, value: any) {
let alreadyDefined =
key in target && Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) {
target[key] = value;
} else {
this._addProp(target, key, value);
this._updateRevNumber(target);
}
this.notifyChange();
}
delete(target: any, key: number | string) {
delete target[key];
this._updateRevNumber(target);
this.notifyChange();
}
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
obj.__owl__ = {
rev: this.rev,
deepRev: this.rev,
parent,
observer: this
};
Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key in obj) {
this._addProp(obj, key, obj[key]);
}
}
_observeArr(arr: Array<any>, parent?: any) {
(<any>arr).__owl__ = {
rev: this.rev,
deepRev: this.rev,
parent,
observer: this
};
Object.defineProperty(arr, "__owl__", { enumerable: false });
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
(<any>arr).__proto__.__observer__ = this;
for (let i = 0, iLen = arr.length; i < iLen; i++) {
this.observe(arr[i], arr);
}
}
_addProp<T extends { __owl__?: any }>(obj: T, key: string | number, value: any) {
var self = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(obj);
value = newVal;
self.observe(newVal, obj);
self.notifyChange();
}
}
});
this.observe(value, obj);
}
_updateRevNumber(target: any) {
this.rev++;
target.__owl__.rev!++;
let parent = target;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== target);
}
}
@@ -1,5 +1,6 @@
import { Context, QWeb, UTILS } from "./qweb_core";
import { QWebExprVar } from "./qweb_expressions";
import { Context } from "./context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
/**
* Owl QWeb Directives
@@ -18,7 +19,7 @@ import { QWebExprVar } from "./qweb_expressions";
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
(<any>UTILS).getFragment = function(str: string): DocumentFragment {
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
@@ -54,10 +55,12 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
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.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
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),`);
@@ -80,6 +83,7 @@ QWeb.addDirective({
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));
@@ -94,6 +98,7 @@ QWeb.addDirective({
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);
@@ -196,12 +201,55 @@ QWeb.addDirective({
// 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();
@@ -214,10 +262,6 @@ QWeb.addDirective({
// todo: handle XML variables...
}
}
// compile sub template
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
@@ -226,6 +270,10 @@ QWeb.addDirective({
ctx.addLine("}");
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
@@ -255,11 +303,11 @@ QWeb.addDirective({
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`);
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
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") {
@@ -277,9 +325,7 @@ QWeb.addDirective({
}
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${
ctx.templateName
}')`
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
nodeCopy.removeAttribute("t-foreach");
+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 + "`";
}
}
+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";
+175 -193
View File
@@ -1,6 +1,8 @@
import { VNode, h } from "./vdom";
import { QWebVar, compileExpr } from "./qweb_expressions";
import { EventBus } from "./event_bus";
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
@@ -64,15 +66,6 @@ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgr
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const DIRECTIVE_NAMES = {
name: 1,
att: 1,
attf: 1,
key: 1
};
const DIRECTIVES: Directive[] = [];
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
@@ -80,14 +73,12 @@ const NODE_HOOKS_PARAMS = {
};
interface Utils {
h: typeof h;
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
export const UTILS: Utils = {
h: h,
const UTILS: Utils = {
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -103,71 +94,121 @@ export const UTILS: Utils = {
}
return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
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) {
throw new Error("Invalid XML in template");
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;
}
let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template } = {};
utils = UTILS;
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;
// the id field is useful to be able to hash qweb instances. The current
// use case is that component's templates are qweb dependant, and need to be
// able to map a qweb instance to a template name.
id = nextID++;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
slots = {};
nextSlotId = 1;
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) {
DIRECTIVES.push(directive);
DIRECTIVE_NAMES[directive.name] = 1;
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
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 => (DIRECTIVE_NAMES[n] = 1));
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
static register(name: string, Component: any) {
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.
@@ -184,12 +225,12 @@ export class QWeb extends EventBus {
}
/**
* Load templates from a xml (as a string). 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.
* 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) {
const doc = parseXML(xmlstr);
addTemplates(xmlstr: string | Document) {
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
@@ -207,7 +248,7 @@ export class QWeb extends EventBus {
this._processTemplate(elem);
const template = {
elem,
fn: (context, extra) => {
fn: function(this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
@@ -268,45 +309,87 @@ export class QWeb extends EventBus {
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate {
/**
* 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 (parentNode) {
ctx.nextID = parentNode + 1;
ctx.parentNode = parentNode;
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.addLine(`let c${parentNode} = extra.parentNode;`);
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 (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
}
if (ctx.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
if (ctx.shouldDefineQWeb) {
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
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};`);
}
}
if (!parentNode) {
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate;
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
@@ -345,6 +428,7 @@ export class QWeb extends EventBus {
// 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;
}
@@ -373,16 +457,16 @@ export class QWeb extends EventBus {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in DIRECTIVE_NAMES)) {
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
}
}
const DIR_N = DIRECTIVES.length;
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
for (let i = 0; i < DIR_N; i++) {
let directive = DIRECTIVES[i];
let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
@@ -419,6 +503,7 @@ export class QWeb extends EventBus {
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] || [];
@@ -456,6 +541,17 @@ export class QWeb extends EventBus {
}
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) {
@@ -530,7 +626,8 @@ export class QWeb extends EventBus {
let formattedValue = v.id || ctx.formatExpression(v);
if (attName === "class") {
formattedValue = `this.utils.toObj(${formattedValue})`;
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
@@ -586,7 +683,9 @@ export class QWeb extends EventBus {
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
nodeKey = ctx.formatExpression(nodeKey);
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
nodeKey = `nodeKey${nodeID}`;
ctx.lastNodeKey = nodeKey;
} else {
nodeKey = nodeID;
}
@@ -632,120 +731,3 @@ export class QWeb extends EventBus {
}
}
}
//------------------------------------------------------------------------------
// 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;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
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;
}
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) {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
}
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(/\{\{.*?\}\}/g);
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 + "`";
}
}
+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;
}
-323
View File
@@ -1,323 +0,0 @@
import { Component, Env } from "./component";
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - a connect function
*
* 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/mutations, 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
//------------------------------------------------------------------------------
type Mutation = ({ state, commit, getters }, ...payload: any) => void;
type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
type Getter = ({ state, getters }, payload) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
mutations?: { [name: string]: Mutation };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
_commitLevel: number = 0;
history: any[] = [];
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
_gettersCache: { [name: string]: {} };
_updateId: number = 1;
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.state = config.state || {};
this.actions = config.actions;
this.mutations = config.mutations;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
this._gettersCache = {};
if (this.debug) {
this.history.push({ state: this.state });
}
const cTypes = ["undefined", "number", "string"];
for (let entry of Object.entries(config.getters || {})) {
const name: string = entry[0];
const func: (...any) => any = entry[1];
this.getters[name] = payload => {
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
this._gettersCache[name] = this._gettersCache[name] || {};
this._gettersCache[name][payload] =
this._gettersCache[name][payload] ||
func({ state: this.state, getters: this.getters }, payload);
return this._gettersCache[name][payload];
}
return func({ state: this.state, getters: this.getters }, payload);
};
}
}
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](
{
commit: this.commit.bind(this),
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
if (result instanceof Promise) {
return new Promise((resolve, reject) => {
result.then(() => resolve());
result.catch(reject);
});
}
}
commit(type: string, ...payload: any): any {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
this._commitLevel++;
this.observer.allowMutations = true;
const res = this.mutations[type].call(
null,
{
commit: this.commit.bind(this),
state: this.state,
getters: this.getters
},
...payload
);
if (this._commitLevel === 1) {
this.observer.allowMutations = false;
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: [...payload]
});
}
}
this._commitLevel--;
return res;
}
/**
* 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() {
this._updateId++;
const current = this._updateId;
this._gettersCache = {};
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, current);
}
}
}
}
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
function revNumber<T extends Object>(o: T): number {
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
return (<any>o).__owl__.rev;
}
return 0;
}
function deepRevNumber<T extends Object>(o: T): number {
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
return (<any>o).__owl__.deepRev;
}
return 0;
}
type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
if (!hashFunction) {
let deep = "deep" in options ? options.deep : true;
let defaultRevFunction = deep ? deepRevNumber : revNumber;
hashFunction = function({ storeProps }, options) {
const { currentStoreProps } = options;
if ("__owl__" in storeProps) {
return defaultRevFunction(storeProps);
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = defaultRevFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
}
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const store = getStore(env);
const ownProps = Object.assign({}, props || {});
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
}
/**
* 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() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = (<HashFunction>hashFunction)(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
};
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
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;
}
+11 -4
View File
@@ -10,7 +10,7 @@
* - debounce
*/
export function whenReady(fn) {
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
if (document.readyState !== "loading") {
resolve();
@@ -48,9 +48,7 @@ export async function loadTemplates(url: string): Promise<string> {
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
let templates = await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
return await result.text();
}
export function escape(str: string | number | undefined): string {
@@ -95,3 +93,12 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
}
};
}
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;
+24 -292
View File
@@ -27,6 +27,21 @@ declare global {
}
}
type Props = Record<string, any>;
type Attrs = Record<string, string | number | boolean>;
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[event: string]: EventListener;
};
export interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
//------------------------------------------------------------------------------
// vnode.ts
//------------------------------------------------------------------------------
@@ -41,7 +56,7 @@ export interface VNode {
key: Key | undefined;
}
interface VNodeData {
export interface VNodeData {
props?: Props;
attrs?: Attrs;
on?: On;
@@ -454,10 +469,6 @@ interface DOMAPI {
nextSibling: (node: Node) => Node;
tagName: (elm: Element) => string;
setTextContent: (node: Node, text: string | null) => void;
getTextContent: (node: Node) => string | null;
isElement: (node: Node) => node is Element;
isText: (node: Node) => node is Text;
isComment: (node: Node) => node is Comment;
}
function createElement(tagName: any): HTMLElement {
@@ -504,22 +515,6 @@ function setTextContent(node: Node, text: string | null): void {
node.textContent = text;
}
function getTextContent(node: Node): string | null {
return node.textContent;
}
function isElement(node: Node): node is Element {
return node.nodeType === 1;
}
function isText(node: Node): node is Text {
return node.nodeType === 3;
}
function isComment(node: Node): node is Comment {
return node.nodeType === 8;
}
const htmlDomApi = {
createElement,
createElementNS,
@@ -531,11 +526,7 @@ const htmlDomApi = {
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment
setTextContent
} as DOMAPI;
//------------------------------------------------------------------------------
@@ -573,13 +564,17 @@ type VNodeChildElement = VNode | string | number | undefined | null;
type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data;
const child = children[i];
if (child === null) {
continue;
}
let childData = child.data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
addNS(childData, (child as VNode).children as VNodes, child.sel);
}
}
}
@@ -621,268 +616,5 @@ export function h(sel: any, b?: any, c?: any): VNode {
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
}
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
type Props = Record<string, any>;
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/module.ts
//------------------------------------------------------------------------------
interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[event: string]: EventListener;
};
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
//------------------------------------------------------------------------------
type Attrs = Record<string, string | number | boolean>;
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);
}
}
}
const classModule = { create: updateClass, update: updateClass } as Module;
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+38 -30
View File
@@ -3,21 +3,23 @@
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
@@ -25,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -46,22 +48,24 @@ exports[`animations t-transition combined with component 1`] = `
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['state'].display) {
//COMPONENT
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
@@ -69,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -91,15 +95,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn, 'jupiler');
utils.transitionInsert(vn, 'jupiler');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn, 'jupiler', rm);
utils.transitionRemove(vn, 'jupiler', rm);
},
};
c1.push({text: \`blue\`});
@@ -110,15 +116,17 @@ exports[`animations t-transition with no delay/duration 1`] = `
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn, 'chimay');
utils.transitionInsert(vn, 'chimay');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn, 'chimay', rm);
utils.transitionRemove(vn, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
File diff suppressed because it is too large Load Diff
+20 -45
View File
@@ -1,10 +1,10 @@
import { Component, Env } from "../src/component";
import { QWeb } from "../src/qweb_core";
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextTick,
patchNextFrame,
renderToDOM,
unpatchNextFrame
@@ -36,7 +36,7 @@ afterEach(() => {
fixture.remove();
});
class Widget extends Component<any, any, any> {}
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
@@ -106,7 +106,7 @@ describe("animations", () => {
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
state = useState({ hide: false });
}
const widget = new TestWidget(env);
@@ -148,7 +148,8 @@ describe("animations", () => {
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget(env);
@@ -163,7 +164,7 @@ describe("animations", () => {
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(widget.refs.span).toBe(spanNode);
expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
@@ -175,10 +176,10 @@ describe("animations", () => {
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
components = { Child: Child };
}
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
}
const widget = new Parent(env);
let def = makeDeferred();
@@ -214,11 +215,11 @@ describe("animations", () => {
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
components = { Child: Child };
state = { display: true };
}
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
state = useState({ display: true });
}
const widget = new Parent(env);
let def = makeDeferred();
@@ -263,28 +264,6 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-transition combined with t-mounted", async () => {
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { flag: false };
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
@@ -297,10 +276,8 @@ describe("animations", () => {
</templates>`
);
class Parent extends Widget {
constructor(parent) {
super(parent);
this.state = { flag: false };
}
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
@@ -356,11 +333,9 @@ describe("animations", () => {
);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
constructor(parent) {
super(parent);
this.state = { flag: false };
}
static components = { Child };
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props default values are also set whenever component is updated 1`] = `"<div>1</div>"`;
exports[`default props default values are also set whenever component is updated 2`] = `"<div>4</div>"`;
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {message:1};
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4)
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
utils.validateProps(w4.constructor, props4)
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component";
import { makeTestFixture, makeTestEnv } from "./helpers";
import { QWeb } from "../src";
import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv } from "../helpers";
import { QWeb } from "../../src/qweb";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -22,7 +22,7 @@ afterEach(() => {
QWeb.dev = dev;
});
class Widget extends Component<any, any, any> {}
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
@@ -45,19 +45,6 @@ describe("props validation", () => {
}).not.toThrow();
});
test("props validation is also done on update props", async () => {
expect.assertions(1);
class TestWidget extends Widget {
static props = ["message"];
}
const w = new TestWidget(env, { message: "bottle" });
try {
await w.__updateProps({});
} catch (e) {
expect(e.message).toBe("Missing props 'message' (component 'TestWidget')");
}
});
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
@@ -234,6 +221,95 @@ describe("props validation", () => {
new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow();
});
test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Child message="1"/>
</div>
<div t-name="Child"><t t-esc="props.message"/></div>
</templates>`);
class Child extends Widget {
static props = ["message"];
}
class App extends Widget {
static components = { Child };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component
// creation, and one at update time.
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("props: list of strings with optional props", async () => {
class TestWidget extends Widget {
static props = ["message", "someProp?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
}).toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("props: can be defined with a boolean", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, {});
}).toThrow();
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: extra props cause an error, part 2", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: optional prop do not cause an error", async () => {
class TestWidget extends Widget {
static props = ["message?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("optional prop do not cause an error if value is undefined", async () => {
class TestWidget extends Widget {
static props = { message: { type: String, optional: true } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: undefined });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: null });
}).toThrow();
});
});
describe("default props", () => {
@@ -250,9 +326,18 @@ describe("default props", () => {
class TestWidget extends Widget {
static defaultProps = { p: 4 };
}
env.qweb.addTemplates(`
<templates>
<div t-name="TestWidget"><t t-esc="props.p"/></div>
</templates>`);
const w = new TestWidget(env, { p: 1 });
await w.__updateProps({});
await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
const fiber = w.__createFiber(false, undefined, undefined, undefined);
await w.__updateProps({}, fiber);
await w.render();
expect(w.props.p).toBe(4);
expect(fixture.innerHTML).toMatchSnapshot();
});
});
@@ -1,4 +1,4 @@
import { EventBus } from "../src/event_bus";
import { EventBus } from "../../src/core/event_bus";
describe("event bus behaviour", () => {
test("can subscribe and be notified", () => {
+466
View File
@@ -0,0 +1,466 @@
import { Observer } from "../../src/core/observer";
import { nextMicroTick } from "../helpers";
describe("observer", () => {
test("properly observe objects", () => {
const observer = new Observer();
const obj: any = observer.observe({});
expect(typeof obj).toBe("object");
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
const obj2: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj2)).toBe(1);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj2.a = 2;
expect(observer.revNumber(obj)).toBe(1);
expect(observer.revNumber(obj2)).toBe(2);
expect(observer.rev).toBe(2);
expect(obj2).toEqual({
a: 2
});
});
test("properly handle null or undefined", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: null, b: undefined });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.b = 5;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
obj.a = null;
obj.b = undefined;
expect(observer.revNumber(obj)).toBe(5);
expect(observer.deepRevNumber(obj)).toBe(5);
expect(observer.rev).toBe(5);
expect(obj).toEqual({
a: null,
b: undefined
});
});
test("properly handle dates", () => {
const observer = new Observer();
const date = new Date();
const obj: any = observer.observe({ date });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
expect(typeof obj.date.getFullYear()).toBe("number");
expect(obj.date).toBe(date);
obj.date = new Date();
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
expect(obj.date).not.toBe(date);
});
test("can change values in array", () => {
const observer = new Observer();
const obj: any = observer.observe({ arr: [1, 2] });
expect(Array.isArray(obj.arr)).toBe(true);
expect(observer.revNumber(obj.arr)).toBe(1);
expect(observer.deepRevNumber(obj.arr)).toBe(1);
expect(observer.rev).toBe(1);
obj.arr[0] = "nope";
expect(observer.revNumber(obj.arr)).toBe(2);
expect(observer.deepRevNumber(obj.arr)).toBe(2);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
});
test("various object property changes", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
// same value again
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
test("properly observe arrays", () => {
const observer = new Observer();
const arr: any = observer.observe([]);
expect(Array.isArray(arr)).toBe(true);
expect(arr.length).toBe(0);
expect(observer.revNumber(arr)).toBe(1);
expect(observer.deepRevNumber(arr)).toBe(1);
expect(observer.rev).toBe(1);
arr.push(1);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(2);
expect(observer.rev).toBe(2);
expect(arr.length).toBe(1);
expect(arr).toEqual([1]);
arr.splice(1, 0, "hey");
expect(observer.revNumber(arr)).toBe(3);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.rev).toBe(3);
expect(arr).toEqual([1, "hey"]);
expect(arr.length).toBe(2);
arr.unshift("lindemans");
//it generates 3 primitive operations
expect(observer.revNumber(arr)).toBe(6);
expect(observer.deepRevNumber(arr)).toBe(6);
expect(observer.rev).toBe(6);
expect(arr).toEqual(["lindemans", 1, "hey"]);
expect(arr.length).toBe(3);
arr.reverse();
//it generates 2 primitive operations
expect(observer.revNumber(arr)).toBe(8);
expect(observer.deepRevNumber(arr)).toBe(8);
expect(observer.rev).toBe(8);
expect(arr).toEqual(["hey", 1, "lindemans"]);
expect(arr.length).toBe(3);
arr.pop(); // one set, one delete
expect(observer.revNumber(arr)).toBe(10);
expect(observer.deepRevNumber(arr)).toBe(10);
expect(observer.rev).toBe(10);
expect(arr).toEqual(["hey", 1]);
expect(arr.length).toBe(2);
arr.shift(); // 2 sets, 1 delete
expect(observer.revNumber(arr)).toBe(13);
expect(observer.deepRevNumber(arr)).toBe(13);
expect(observer.rev).toBe(13);
expect(arr).toEqual([1]);
expect(arr.length).toBe(1);
});
test("object pushed into arrays are observed", () => {
const observer = new Observer();
const arr: any = observer.observe([]);
expect(observer.rev).toBe(1);
arr.push({ kriek: 5 });
expect(observer.rev).toBe(2);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.revNumber(arr[0])).toBe(2);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.revNumber(arr[0])).toBe(3);
});
test("set new property on observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe({ a: 1 });
expect(observer.notifyCB).toBeCalledTimes(0);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
state.b = 8;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(state.b).toBe(8);
});
test("delete property from observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe({ a: 1, b: 8 });
observer.observe(state);
expect(observer.notifyCB).toBeCalledTimes(0);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
delete state.b;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(state).toEqual({ a: 1 });
});
test("set element in observed array", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe(["a"]);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
state[1] = "b";
await nextMicroTick();
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]);
});
test("properly observe arrays in object", () => {
const observer = new Observer();
const state: any = observer.observe({ arr: [] });
expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.deepRevNumber(state.arr)).toBe(1);
expect(state.arr.length).toBe(0);
state.arr.push(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.deepRevNumber(state.arr)).toBe(2);
expect(state.arr.length).toBe(1);
});
test("properly observe objects in array", () => {
const observer = new Observer();
const state: any = observer.observe({ arr: [{ something: 1 }] });
observer.observe(state);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.revNumber(state.arr[0])).toBe(1);
state.arr[0].something = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.revNumber(state.arr[0])).toBe(2);
});
test("properly observe objects in object", () => {
const observer = new Observer();
const state: any = observer.observe({ a: { b: 1 } });
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
expect(observer.revNumber(state.a)).toBe(1);
state.a.b = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.revNumber(state.a)).toBe(2);
});
test("reobserve new object values", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: 1 });
expect(observer.rev).toBe(1);
expect(observer.revNumber(obj)).toBe(1);
obj.a = { b: 2 };
expect(observer.rev).toBe(2);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj.a)).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj.a)).toBe(3);
});
test("deep observe misc changes", () => {
const observer = new Observer();
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
state.o.a = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(2);
state.arr.push(2);
expect(observer.rev).toBe(3);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(3);
state.n = 155;
expect(observer.rev).toBe(4);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(4);
});
test("properly handle already observed state", () => {
const observer = new Observer();
const obj1: any = observer.observe({ a: 1 });
const obj2: any = observer.observe({ b: 1 });
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(1);
obj1.a = 2;
obj2.b = 3;
expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(2);
obj2.b = obj1;
expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(3);
});
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = observer.observe({});
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.aku = "always finds annoying problems";
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.aku = "always finds good problems";
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
test("properly handle swapping elements", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: { arr: [] }, b: 1 });
// swap a and b
const b = obj.b;
obj.b = obj.a;
obj.a = b;
expect(observer.rev).toBe(3);
// push something into array to make sure it works
obj.b.arr.push("blanche");
expect(observer.rev).toBe(4);
});
test("properly handle assigning observed obj containing array", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: { arr: [], val: "test" } });
expect(observer.rev).toBe(1);
obj.a = { ...obj.a, val: "test2" };
expect(observer.rev).toBe(2);
// push something into array to make sure it works
obj.a.arr.push("blanche");
expect(observer.rev).toBe(3);
});
test("accept cycles in observed state", () => {
const observer = new Observer();
let obj1: any = {};
let obj2: any = { b: obj1, key: 1 };
obj1.a = obj2;
obj1 = observer.observe(obj1);
obj2 = obj1.a;
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(1);
obj2.key = 3;
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(2);
});
test("call callback when state is changed", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });
expect(observer.notifyCB).toBeCalledTimes(0);
obj.a = 2;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
obj.b.c = 3;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(2);
obj.d[0].e = 5;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(3);
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = observer.observe({ a: 1 });
expect(() => {
obj.a = 2;
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
});
test("throw error when state is mutated in array if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = observer.observe({ a: [1] });
expect(() => {
obj.a.push(2);
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
});
});
+50 -10
View File
@@ -1,9 +1,31 @@
import { Env } from "../src/component";
import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
import { Env } from "../src/component/component";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { patch } from "../src/vdom";
import "../src/qweb_directives";
import "../src/qweb_extensions";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
import "../src/component/directive";
// Some static cleanup
let nextSlotId;
let slots;
let nextId;
let TEMPLATES;
beforeEach(() => {
nextSlotId = QWeb.nextSlotId;
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
});
afterEach(() => {
QWeb.nextSlotId = nextSlotId;
QWeb.slots = slots;
QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES;
});
// helpers
export function nextMicroTick(): Promise<void> {
return Promise.resolve();
}
@@ -68,24 +90,42 @@ export function renderToDOM(
return result.elm as HTMLElement;
}
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML;
/**
* Render a template to an html string. The big difference with the
* renderToString method in QWeb is that we use the renderToDom method, which
* snapshots the resulting template function. Doing so gives us a large body
* of reference to evaluate changes in the generated QWeb code.
*
* Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb.
*/
export function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result;
}
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
// is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards.
let nextFrame = UTILS.nextFrame;
let nextFrame = QWeb.utils.nextFrame;
export function patchNextFrame(f: Function) {
UTILS.nextFrame = (cb: () => void) => {
QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb));
};
}
export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame;
QWeb.utils.nextFrame = nextFrame;
}
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
+359
View File
@@ -0,0 +1,359 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import {
useState,
onMounted,
onWillUnmount,
useRef,
onPatched,
onWillPatch,
useSubEnv
} from "../src/hooks";
import { xml } from "../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("hooks", () => {
test("can use a state hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3</div>");
});
test("can use onMounted, onWillUnmount", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("two different call to mounted/willunmount should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onMounted(() => {
steps.push("hook:mounted" + i);
});
onWillUnmount(() => {
steps.push("hook:willunmount" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual([
"hook:mounted1",
"hook:mounted2",
"hook:willunmount2",
"hook:willunmount1"
]);
});
test("useRef hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
onWillPatch(() => {
steps.push("willPatch");
});
onPatched(() => {
steps.push("patched");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["willPatch", "patched"]);
});
test("patched, willPatch, onPatched, onWillPatch order", async () => {
const steps: string[] = [];
function useMyHook() {
onPatched(() => {
steps.push("hook:patched");
});
onWillPatch(() => {
steps.push("hook:willPatch");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
willPatch() {
steps.push("comp:willPatch");
}
patched() {
steps.push("comp:patched");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
});
test("two different call to willPatch/patched should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onPatched(() => {
steps.push("hook:patched" + i);
});
onWillPatch(() => {
steps.push("hook:willPatch" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div>hey2</div>");
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
});
describe("autofocus hook", () => {
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);
}
test("simple input", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<input t-ref="input2"/>
</div>`;
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
test("input in a t-if", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<t t-if="state.flag"><input t-ref="input2"/></t>
</div>`;
state = useState({ flag: false });
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
component.state.flag = true;
await nextTick();
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
});
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new TestComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val");
expect(component.env).toHaveProperty("val");
});
test("parent and child env", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 5 });
}
}
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child}
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new Parent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe( "<div>3<div>5</div></div>");
});
});
-394
View File
@@ -1,394 +0,0 @@
import { Observer } from "../src/observer";
import { nextMicroTick } from "./helpers";
describe("observer", () => {
test("properly observe objects", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
const ob2: any = { a: 1 };
observer.observe(ob2);
expect(ob2.__owl__.rev).toBe(1);
ob2.a = 2;
expect(observer.rev).toBe(2);
expect(ob2.__owl__.rev).toBe(2);
ob2.b = 3;
expect(observer.rev).toBe(2);
expect(ob2.__owl__.rev).toBe(2);
observer.set(ob2, "b", 4);
expect(observer.rev).toBe(3);
expect(ob2.__owl__.rev).toBe(3);
});
test("properly handle null or undefined", () => {
const observer = new Observer();
const obj: any = { a: null, b: undefined };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 3;
expect(obj.__owl__.rev).toBe(2);
obj.b = 5;
expect(obj.__owl__.rev).toBe(3);
obj.a = null;
obj.b = undefined;
expect(obj.__owl__.rev).toBe(5);
});
test("can change values in array", () => {
const observer = new Observer();
const obj: any = { arr: [1, 2] };
observer.observe(obj);
expect(obj.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
obj.arr[0] = "nope";
expect(obj.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
observer.set(obj.arr, 0, "yep");
expect(obj.arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
});
test("various object property changes", () => {
const observer = new Observer();
const obj: any = { a: 1 };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
obj.a = 2;
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
// same value again
obj.a = 2;
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
obj.a = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
});
test("properly observe arrays", () => {
const observer = new Observer();
const arr: any = [];
observer.observe(arr);
expect(arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(arr.length).toBe(0);
arr.push(1);
expect(arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(arr.length).toBe(1);
arr.splice(1, 0, "hey");
expect(arr.__owl__.rev).toBe(3);
expect(observer.rev).toBe(3);
expect(arr.length).toBe(2);
arr.unshift("lindemans");
expect(arr.__owl__.rev).toBe(4);
arr.reverse();
expect(arr.__owl__.rev).toBe(5);
arr.pop();
expect(arr.__owl__.rev).toBe(6);
arr.shift();
expect(arr.__owl__.rev).toBe(7);
arr.sort();
expect(arr.__owl__.rev).toBe(8);
expect(arr).toEqual([1]);
});
test("object pushed into arrays are observed", () => {
const observer = new Observer();
const arr: any = [];
observer.observe(arr);
expect(observer.rev).toBe(1);
arr.push({ kriek: 5 });
expect(observer.rev).toBe(2);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(2);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(3);
});
test("set new property on observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = { a: 1 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.set(state, "b", 8);
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state.b).toBe(8);
});
test("delete property from observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = { a: 1, b: 8 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.delete(state, "b");
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual({ a: 1 });
});
test("set element in observed array", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = ["a"];
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.set(state, 1, "b");
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]);
});
test("properly observe arrays in object", () => {
const observer = new Observer();
const state: any = { arr: [] };
observer.observe(state);
expect(state.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(state.arr.length).toBe(0);
state.arr.push(1);
expect(state.arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(state.arr.length).toBe(1);
});
test("properly observe objects in array", () => {
const observer = new Observer();
const state: any = { arr: [{ something: 1 }] };
observer.observe(state);
expect(state.arr.__owl__.rev).toBe(1);
expect(state.arr[0].__owl__.rev).toBe(1);
state.arr[0].something = 2;
expect(state.arr.__owl__.rev).toBe(1);
expect(state.arr[0].__owl__.rev).toBe(2);
});
test("properly observe objects in object", () => {
const observer = new Observer();
const state: any = { a: { b: 1 } };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(state.a.__owl__.rev).toBe(1);
state.a.b = 2;
expect(state.__owl__.rev).toBe(1);
expect(state.a.__owl__.rev).toBe(2);
});
test("reobserve new object values", () => {
const observer = new Observer();
const obj: any = { a: 1 };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
obj.a = { b: 2 };
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
// we start at 2 because it is the new observer rev number
expect(obj.a.__owl__.rev).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(3);
});
test("deep observe misc changes", () => {
const observer = new Observer();
const state: any = { o: { a: 1 }, arr: [1], n: 13 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(1);
state.o.a = 2;
expect(observer.rev).toBe(2);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(2);
state.arr.push(2);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(3);
state.n = 155;
expect(state.__owl__.rev).toBe(2);
expect(state.__owl__.deepRev).toBe(4);
});
test("properly handle already observed state", () => {
const observer = new Observer();
const obj1: any = { a: 1 };
const obj2: any = { b: 1 };
observer.observe(obj1);
observer.observe(obj2);
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(1);
obj1.a = 2;
obj2.b = 3;
expect(obj1.__owl__.rev).toBe(2);
expect(obj2.__owl__.rev).toBe(2);
obj2.b = obj1;
expect(obj1.__owl__.rev).toBe(2);
expect(obj2.__owl__.rev).toBe(3);
});
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(obj.__owl__.deepRev).toBe(1);
observer.set(obj, "aku", "always finds annoying problems");
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
expect(obj.__owl__.deepRev).toBe(2);
observer.set(obj, "aku", "always finds good problems");
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
expect(obj.__owl__.deepRev).toBe(3);
});
test("properly handle swapping elements", () => {
const observer = new Observer();
const obj: any = { a: { arr: [] }, b: 1 };
observer.observe(obj);
// swap a and b
const b = obj.b;
obj.b = obj.a;
obj.a = b;
expect(observer.rev).toBe(3);
// push something into array to make sure it works
obj.b.arr.push("blanche");
expect(observer.rev).toBe(4);
});
test("properly handle assigning observed obj containing array", () => {
const observer = new Observer();
const obj: any = { a: { arr: [], val: "test" } };
observer.observe(obj);
obj.a = { ...obj.a, val: "test2" };
expect(observer.rev).toBe(2);
// push something into array to make sure it works
obj.a.arr.push("blanche");
expect(observer.rev).toBe(3);
});
test("accept cycles in observed state", () => {
const observer = new Observer();
const obj1: any = {};
const obj2: any = { b: obj1, key: 1 };
obj1.a = obj2;
observer.observe(obj1);
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(1);
obj2.key = 3;
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(2);
});
test("call callback when state is changed", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const obj: any = { a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 };
observer.observe(obj);
expect(observer.notifyCB).toBeCalledTimes(0);
obj.a = 2;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
obj.b.c = 3;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(2);
obj.d[0].e = 5;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(3);
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = { a: 1 };
observer.observe(obj);
expect(() => {
obj.a = 2;
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
});
test("throw error when state is mutated in array if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = { a: [1] };
observer.observe(obj);
expect(() => {
obj.a.push(2);
}).toThrow("Array cannot be changed here");
});
});
File diff suppressed because it is too large Load Diff
+169 -5
View File
@@ -1,5 +1,5 @@
import { QWeb } from "../src/qweb_core";
import { normalize, renderToDOM, renderToString, trim } from "./helpers";
import { QWeb } from "../../src/qweb/index";
import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -11,6 +11,7 @@ import { normalize, renderToDOM, renderToString, trim } from "./helpers";
let qweb: QWeb;
beforeEach(() => {
QWeb.TEMPLATES = {};
qweb = new QWeb();
});
@@ -48,6 +49,11 @@ describe("static templates", () => {
qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
});
test("ignore comments", () => {
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello owl</div>");
});
});
describe("error handling", () => {
@@ -501,6 +507,13 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("t-call with t-if", () => {
qweb.addTemplate("sub", "<span>ok</span>");
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
});
test("t-call not allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "<t>ok</t>");
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
@@ -566,6 +579,103 @@ describe("t-call (template calling", () => {
const expected = "<div>ok</div>";
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
});
test("call with several sub nodes on same line", () => {
qweb.addTemplates(`
<templates>
<div t-name="SubTemplate">
<t t-raw="0"/>
</div>
<div t-name="main">
<t t-call="SubTemplate">
<span>hey</span> <span>yay</span>
</t>
</div>
</templates>
`);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("recursive template, part 1", () => {
qweb.addTemplates(`
<templates>
<div t-name="recursive">
<span>hey</span>
<t t-if="false">
<t t-call="recursive"/>
</t>
</div>
</templates>
`);
const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("recursive template, part 2", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("recursive template, part 3", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("t-call, global templates", () => {
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
qweb.addTemplate("john", `<span>desk</span>`);
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
});
describe("foreach", () => {
@@ -982,14 +1092,14 @@ describe("t-ref", () => {
test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs });
renderToDOM(qweb, "test", { __owl__: { refs } });
expect(refs.myspan.tagName).toBe("SPAN");
});
test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs, id: 3 });
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
expect(refs.myspan3.tagName).toBe("SPAN");
});
@@ -1004,7 +1114,7 @@ describe("t-ref", () => {
</div>`
);
let refs: any = {};
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
});
});
@@ -1171,3 +1281,57 @@ describe("debugging", () => {
console.log = consoleLog;
});
});
describe("update on event bus", () => {
test("two consecutive forceUpdates only causes one listener update", async () => {
const fn = jest.fn();
qweb.on("update", null, fn);
qweb.forceUpdate();
qweb.forceUpdate();
expect(fn).toBeCalledTimes(0);
await nextTick();
expect(fn).toBeCalledTimes(1);
});
});
describe("global template registration", () => {
test("can register template globally", () => {
expect.assertions(5);
let qweb = new QWeb();
try {
qweb.render("mytemplate");
} catch (e) {
expect(e.message).toMatch("Template mytemplate does not exist");
}
expect(qweb.templates.mytemplate).toBeUndefined();
QWeb.registerTemplate("mytemplate", "<div>global</div>");
expect(qweb.templates.mytemplate).toBeDefined();
const vnode = qweb.render("mytemplate");
expect(vnode.sel).toBe("div");
expect((vnode as any).children[0].text).toBe("global");
});
});
describe("properly support svg", () => {
test("add proper namespace to svg", () => {
qweb.addTemplate(
"test",
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
);
expect(renderToString(qweb, "test")).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
);
});
test("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg
qweb.addTemplate(
"test",
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
);
expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
});
@@ -1,4 +1,4 @@
import { compileExpr, tokenize } from "../src/qweb_expressions";
import { compileExpr, tokenize } from "../../src/qweb/expression_parser";
describe("tokenizer", () => {
test("simple tokens", () => {
+84
View File
@@ -0,0 +1,84 @@
import { Component } from "../../src/component/component";
import { Link } from "../../src/router/Link";
import { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
describe("Link component", () => {
let fixture: HTMLElement;
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
});
afterEach(() => {
fixture.remove();
if (router) {
router.destroy();
}
router = null;
});
test("can render simple cases", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Link to="'about'">About</Link>
</div>
</templates>
`);
class App extends Component<any, any> {
static components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
expect(window.location.pathname).toBe("/users");
fixture.querySelector("a")!.click();
await nextTick();
expect(window.location.pathname).toBe("/about");
expect(fixture.innerHTML).toBe(
'<div><a href="/about" class="router-link-active">About</a></div>'
);
expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot();
});
test("do not redirect if right clicking", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Link to="'about'">About</Link>
</div>
</templates>
`);
class App extends Component<any, any> {
static components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
await app.mount(fixture);
expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", {
button: 1
});
fixture.querySelector("a")!.dispatchEvent(evt);
await nextTick();
expect(window.location.pathname).toBe("/users");
});
});
+105
View File
@@ -0,0 +1,105 @@
import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router";
import { RouteComponent } from "../../src/router/RouteComponent";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
describe("RouteComponent", () => {
let fixture: HTMLElement;
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
});
afterEach(() => {
fixture.remove();
if (router) {
router.destroy();
}
router = null;
});
test("can render simple cases", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="About">About</span>
<span t-name="Users">Users</span>
</templates>
`);
class About extends Component<any, any> {}
class Users extends Component<any, any> {}
class App extends Component<any, any> {
static components = { RouteComponent };
}
const routes = [
{ name: "about", path: "/about", component: About },
{ name: "users", path: "/users", component: Users }
];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "about" });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
await router.navigate({ to: "users" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot();
});
test("can render parameterized route", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="Book">Book <t t-esc="props.title"/></span>
</templates>
`);
class Book extends Component<any, any> {}
class App extends Component<any, any> {
static components = { RouteComponent };
}
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984" } });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
});
test("can render parameterized route with suffixes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates>
`);
class Book extends Component<any, any> {
get incVal() {
return this.props.val + 1;
}
}
class App extends Component<any, any> {
static components = { RouteComponent };
}
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
});
});
+15
View File
@@ -0,0 +1,15 @@
import { Router } from "../../src/router/Router";
import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router {
destroy() {
window.removeEventListener("popstate", (this as any)._listener);
// remove component defined inroutes
for (let key in QWeb.components) {
if (key.startsWith("__component__")) {
delete QWeb.components[key];
}
}
}
}
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let owner = context;
var h = this.h;
let _1 = utils.toObj({'router-link-active':context['isActive']});
var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3);
result = vn3;
if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
}
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) {
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
}
return vn3;
}"
`;
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let result;
var h = this.h;
if (context['routeComponent']) {
//COMPONENT
let key4 = 'key' + context['env'].router.currentRouteName;
let def2;
let templateId5 = key4;
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
let vn6 = {};
result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
def2 = w3.__owl__.currentFiber.promise;
} else {
w3.destroy();
w3 = false;
}
}
if (!w3) {
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
def2 = w3.__prepare(extra.fiber, undefined, undefined);
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
} else {
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
}
extra.promises.push(def2);
}
return result;
}"
`;
+258
View File
@@ -0,0 +1,258 @@
import { Destination, RouterEnv, Route } from "../../src/router/Router";
import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
env = <RouterEnv>makeTestEnv();
window.history.pushState({}, "/", "/");
});
afterEach(() => {
if (router) {
router.destroy();
}
router = null;
});
describe("router miscellaneous", () => {
test("validate routes shape", () => {
expect(() => {
router = new TestRouter(env, [
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination }
]);
}).toThrow(`Invalid destination: {"abc":"hey"}`);
});
test("navigate to same route but with different params should trigger update", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
env.qweb.forceUpdate = jest.fn();
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.pathname).toBe("/users/3");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
await router.navigate({ to: "users", params: { id: 5 } });
expect(window.location.pathname).toBe("/users/5");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
});
test("changing url to same route but with different params should trigger update (hash mode)", async () => {
env.qweb.forceUpdate = jest.fn();
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
await router.start();
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.hash).toBe("#/users/3");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
window.location.hash = "/users/5";
window.dispatchEvent(new Event("hashchange"));
await nextTick();
expect(window.location.hash).toBe("#/users/5");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
});
});
describe("routeToPath", () => {
test("simple non parameterized path", () => {
router = new TestRouter(env, []);
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("/abc");
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("/abc/def");
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("/abc");
});
test("simple parameterized path", () => {
router = new TestRouter(env, []);
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("/abc/34");
});
test("simple non parameterized path, mode = hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("#/abc");
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("#/abc/def");
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("#/abc");
});
test("simple parameterized path, mode=hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("#/abc/34");
});
});
describe("destToPath", () => {
test("validate destination shape", () => {
router = new TestRouter(env, [{ name: "someroute", path: "/some/path" }]);
expect(() => {
router!.destToPath({ abc: 123 } as Destination);
}).toThrow('Invalid destination: {"abc":123}');
expect(() => {
router!.destToPath({ to: "someroute" } as Destination);
}).not.toThrow();
expect(() => {
router!.destToPath({ path: "/someroute", to: "otherroute" } as Destination);
}).toThrow();
});
});
describe("getRouteParams", () => {
test("properly match simple routes", () => {
router = new TestRouter(env, []);
// simple route
expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
// no match
expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
// fallback route
expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
});
test("properly match simple routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
// simple route
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
// no match
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
// fallback route
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
});
test("match some parameterized routes", () => {
router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3"
});
});
test("match some parameterized routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{
id: "3"
}
);
});
test("can convert to number if needed", () => {
router = new TestRouter(env, []);
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3
});
});
test("can convert to number if needed, mode: hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({
id: 3
});
});
});
describe("redirect", () => {
test("can redirect to other route", async () => {
router = new TestRouter(env, [
{ name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } }
]);
await router.start();
expect(window.location.pathname).toBe("/");
await router.navigate({ to: "routeb" });
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("routea");
});
test("can redirect to other path", async () => {
router = new TestRouter(env, [
{ name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } }
]);
router.navigate = jest.fn(router.navigate);
await router.start();
await router.navigate({ to: "routeb" });
// once because we ask for it, once because it is redirected
// expect(router.navigate).toBeCalledTimes(2);
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("routea");
});
});
describe("beforeRouteEnter", () => {
test("navigation guard is called and properly handle return true", async () => {
expect(window.location.pathname).not.toBe("/some/path");
const guard = jest.fn(() => true);
router = new TestRouter(env, [{ name: "route", path: "/some/path", beforeRouteEnter: guard }]);
await router.start();
await router.navigate({ to: "route" });
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("route");
});
test("navigation is cancelled if guard return false", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => false);
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
]);
await router.start();
await router.navigate({ to: "routea" });
expect(window.location.pathname).toBe("/some/patha");
const result = await router.navigate({ to: "routeb" });
expect(result).toBe(false);
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/patha");
expect(router.currentRouteName).toBe("routea");
});
test("navigation is redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {
return { to: "routec" };
});
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
{ name: "routec", path: "/some/pathc" }
]);
await router.start();
const result = await router.navigate({ to: "routea" });
expect(result).toBe(true);
expect(window.location.pathname).toBe("/some/patha");
await router.navigate({ to: "routeb" });
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/pathc");
expect(router.currentRouteName).toBe("routec");
});
test("navigation is initially redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {
return { to: "otherroute" };
});
router = new TestRouter(env, [
{ name: "landing", path: "/", beforeRouteEnter: guard },
{ name: "otherroute", path: "/some/other/route" }
]);
expect(window.location.pathname).toBe("/");
await router.start();
expect(window.location.pathname).toBe("/some/other/route");
});
});
-1390
View File
File diff suppressed because it is too large Load Diff
@@ -15,3 +15,7 @@ exports[`connecting a component to store deep and shallow connecting a component
exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
File diff suppressed because it is too large Load Diff
+328
View File
@@ -0,0 +1,328 @@
import { Env } from "../../src/component/component";
import { Store, Getter } from "../../src/store/store";
import { nextTick, nextMicroTick } from "../helpers";
describe("basic use", () => {
test("dispatch an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc", 14);
expect(store.state.n).toBe(15);
});
test("dispatch an action with positional arguments", () => {
const state = { n1: 1, n2: 1, n3: 1 };
const actions = {
batchInc({ state }, delta1, delta2, delta3) {
state.n1 += delta1;
state.n2 += delta2;
state.n3 += delta3;
}
};
const store = new Store({ state, actions });
expect(store.state.n1).toBe(1);
expect(store.state.n2).toBe(1);
expect(store.state.n3).toBe(1);
store.dispatch("batchInc", 14, 30, 88);
expect(store.state.n1).toBe(15);
expect(store.state.n2).toBe(31);
expect(store.state.n3).toBe(89);
});
test("can dispatch an action in an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
inc100({ dispatch }) {
dispatch("inc", 100);
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc100");
expect(store.state.n).toBe(101);
});
test("return data from dispatching an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }) {
return ++state.n;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
const res = store.dispatch("inc");
expect(store.state.n).toBe(2);
expect(res).toBe(2);
});
test("dispatch allow synchronizing between actions", async () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
setN({ state }, n) {
state.n = n;
},
async dosomething({ dispatch }) {
await dispatch("setTo10");
dispatch("inc", 3);
},
async setTo10({ dispatch }) {
await Promise.resolve();
dispatch("setN", 10);
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("dosomething");
expect(store.state.n).toBe(1);
await nextTick();
expect(store.state.n).toBe(13);
});
test("return data from dispatching an action", async () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
setN({ state }, n) {
state.n = n;
},
async dosomething({ dispatch }) {
const val = await dispatch("setTo10");
dispatch("inc", val);
},
async setTo10({ dispatch }) {
await Promise.resolve();
dispatch("setN", 10);
return 5;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("dosomething");
expect(store.state.n).toBe(1);
await nextTick();
expect(store.state.n).toBe(15);
});
test("env is given to actions", () => {
expect.assertions(1);
const someEnv = <Env>{};
const actions = {
someaction({ env }) {
expect(env).toBe(someEnv);
}
};
const store = new Store({ state: {}, actions, env: someEnv });
store.dispatch("someaction");
});
test("can have getters from store", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
};
const getters: { [key: string]: Getter } = {
beerTasterName({ state }, beerID) {
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
return state.beers[1].name;
}
};
const store = new Store({ state, actions: {}, getters });
expect(store.getters).toBeDefined();
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
});
test("getters given to actions", async () => {
expect.assertions(3);
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
};
const getters = {
beerTasterName({ state }, beerID) {
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
return state.beers[1].name;
}
};
const actions = {
action({ getters }) {
expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron");
}
};
const store = new Store({ state, actions, getters });
store.dispatch("action");
});
test("can use getters inside a getter", () => {
const getters = {
a({ getters }) {
return `${getters.b()}${getters.c(1)}`;
},
b() {
return "b";
},
c({}, i) {
return `c${i}`;
}
};
const store = new Store({ getters, state: {}, actions: {} });
expect(store.getters.a()).toBe("bc1");
});
});
describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => {
const actions = {
donothing() {}
};
const store = new Store({ state: {}, actions });
const state = store.state;
store.dispatch("donothing");
expect(store.state).toBe(state);
});
test("can use array properties in mutations", () => {
expect.assertions(3);
const state = { a: [1, 2, 3] };
const actions = {
m({ state }) {
expect(state.a.length).toBe(3);
const l = state.a.push(53);
expect(l).toBe(4);
}
};
const store = new Store({ state, actions });
store.dispatch("m");
expect(store.state.a).toEqual([1, 2, 3, 53]);
});
test("can use object assign in store", async () => {
const actions = {
dosomething({ state }) {
Object.assign(state.westmalle, { a: 3, b: 4 });
}
};
const store = new Store({
state: { westmalle: { a: 1, b: 2 } },
actions
});
store.dispatch("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
});
test("aku reactive store state 1", async () => {
const actions = {
inc({ state }) {
state.counter++;
}
};
const state = { counter: 0 };
const store = new Store({ state, actions });
expect(store.state.counter).toBe(0);
store.dispatch("inc", {});
expect(store.state.counter).toBe(1);
});
});
describe("updates triggered by the store", () => {
test("multiple commits trigger one update", async () => {
let updateCounter = 0;
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
store.dispatch("inc", 14);
expect(updateCounter).toBe(0);
store.dispatch("inc", 50);
expect(updateCounter).toBe(0);
await nextMicroTick();
expect(updateCounter).toBe(1);
});
test("empty commits do not trigger updates", async () => {
let updateCounter = 0;
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
noop() {},
noop2({ state }) {
const val = state.n;
state.n = val;
}
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
store.dispatch("noop");
await nextMicroTick();
expect(updateCounter).toBe(0);
store.dispatch("inc", 50);
await nextMicroTick();
expect(updateCounter).toBe(1);
store.dispatch("noop2");
await nextMicroTick();
expect(updateCounter).toBe(1);
});
});
+8 -11
View File
@@ -1,4 +1,5 @@
import { h, patch, init } from "../src/vdom";
import { h, patch } from "../src/vdom";
import { init, addNS } from "../src/vdom/vdom";
function map(list, fn) {
var ret: any[] = [];
@@ -205,19 +206,15 @@ describe("snabbdom", function() {
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
// verify that svg tag automatically gets svg namespace
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
.elm;
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
// need to add namespace manually. it is usually done by the template
// compiler
addNS(vnode.data, (vnode as any).children, vnode.sel);
elm = patch(vnode0, vnode).elm;
expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
// verify that svg tag with extra selectors gets svg namespace
elm = patch(vnode0, h("svg#some-id")).elm;
expect(elm.namespaceURI).toBe(SVGNamespace);
// verify that non-svg tag beginning with 'svg' does NOT get namespace
elm = patch(vnode0, h("svg-custom-el")).elm;
expect(elm.namespaceURI).not.toBe(SVGNamespace);
});
test("receives classes in selector", function() {
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.16.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
owl.Observer.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.17.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.18.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+153
View File
@@ -0,0 +1,153 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.21.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+11 -10
View File
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -15,7 +16,7 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
@@ -31,8 +32,9 @@ class Message extends owl.Component {
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -107,12 +109,11 @@ class App extends owl.Component {
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const setState = owl.Observer.set;
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
setState(messages, i, msg);
messages[i] = msg;
}
});
}
@@ -130,12 +131,12 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
this.logRef.el.innerHTML = "";
}
}
+2 -2
View File
@@ -12,11 +12,11 @@
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
+1 -3
View File
@@ -1,9 +1,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
+4
View File
@@ -36,6 +36,10 @@
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+1 -3
View File
@@ -1,9 +1,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.title,
+128 -126
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
@@ -150,145 +150,24 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.components = { TabbedEditor };
this.state = {
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
};
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.refs.content.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.refs.content.innerHTML = "";
this.refs.content.appendChild(subiframe);
}
setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
this.state[ev.detail.type] = ev.detail.value;
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = {
this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
};
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editor = null;
this.editorNode = useRef("editor");
}
mounted() {
this.editor = this.editor || ace.edit(this.refs.editor);
this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
@@ -362,6 +241,129 @@ class TabbedEditor extends owl.Component {
}
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.state = useState({
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.content.el.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.content.el.innerHTML = "";
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
this.state[ev.detail.type] = ev.detail.value;
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
}
App.components = { TabbedEditor };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
+13 -9
View File
@@ -2,9 +2,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
@@ -175,16 +173,22 @@ body {
}
.right-pane .note {
font-size: 20px;
font-size: 18px;
text-align: left;
margin-top: 20px;
padding: 5%;
}
.right-pane .error {
height: 100%;
padding-top: 40%;
font-size: 30px;
width: 90%;
padding-top: 30%;
font-size: 18px;
color: darkred;
text-align: center;
padding-left: 30px;
padding-right: 30px;
margin-left: 5%;
}
.right-pane .error pre {
overflow: auto;
width: 100%;
}
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -45,10 +45,15 @@
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<div t-if="state.error" class="error">
<t t-esc="state.error"/>
<h3>Error</h3>
<pre t-esc="state.error"/>
</div>
<div class="content" t-ref="content"/>
</div>
+2 -2
View File
@@ -5,7 +5,7 @@
"noImplicitThis": true,
"removeComments": false,
"declaration": true,
"target": "es5",
"target": "esnext",
"outDir": "dist",
"alwaysStrict": true,
"noUnusedLocals": true,
@@ -15,5 +15,5 @@
"strictPropertyInitialization": true,
"strictNullChecks": true
},
"include": ["src/*.ts"]
"include": ["src/**/*.ts","src/*.ts"]
}