Compare commits

...

159 Commits

Author SHA1 Message Date
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
Géry Debongnie d381e85d94 [REL] bump to v0.16.0 2019-06-28 11:20:29 +02:00
Géry Debongnie af7520d869 [REF] *: reformat with prettier and printWidth=100
closes #214
2019-06-28 10:33:13 +02:00
Alexandre Kühn 0095bfa61f [IMP] store: positional arguments with dispatch/commit 2019-06-28 10:12:56 +02:00
Géry Debongnie 1b12cf9b91 [FIX] qweb/component: better handling of class attribute
This changes requires using the class module of vdom.

closes #192
2019-06-28 09:49:41 +02:00
Géry Debongnie dbfc7e4acd [FIX] store/connect: fix bugs with parent/child connected
There were big issues when we use parent/child connected widgets.

- children was rendered twice (and mapStoreToProps was called twice)
- if children was supposed to be destroyed, it was rendered once.

We solve them in this commit by waiting for parent widgets to be ready
before updating children.

closes #216
2019-06-28 09:45:43 +02:00
Pierre Paridans e838e879c0 [FIX] qweb: add missing not equals operators
Implement "!=" and "!==" operator support in QWeb.
2019-06-26 16:14:32 +02:00
Géry Debongnie ee63f6bb0f [IMP] playground: log error in console 2019-06-26 15:50:10 +02:00
Géry Debongnie 16bbb8bc9f [FIX] qweb/component: better handling for t-debug
- fix issue with t-debug on a t-set (defining a slot)
- put t-debug and t-log at a very low (high) priority to make sure they
  are executed before other directives

closes #201
2019-06-25 14:03:05 +02:00
Alexandre Kühn 4a5db0c283 [FIX] qweb: t-component + class with extra whitespaces
Crashed due to `DOMTokenList.add(string)` requiring non-empty string.
2019-06-25 13:08:14 +02:00
Géry Debongnie cc50a2e3bb [FIX] playground: fix some issues with tab handling
closes #200
2019-06-25 09:38:00 +02:00
Géry Debongnie 1e2b204fdd [FIX] component: default slot now works with textnodes
closes #209
2019-06-25 09:37:26 +02:00
Géry Debongnie 4a889b7b6b [IMP] qweb: add option to allow duplicate templates 2019-06-24 22:41:02 +02:00
Géry Debongnie 34695883c2 [REF] component: use __ for private methods
closes #202
2019-06-24 13:42:20 +02:00
Géry Debongnie 3d2e2a1873 [FIX] components: ignore slot if no definition is given
closes #203
2019-06-24 13:38:32 +02:00
Géry Debongnie af6aca83a2 [IMP] components: allow multiple roots in slots
closes #199
2019-06-24 13:36:11 +02:00
Géry Debongnie 8c8ffb6a6b [IMP] qweb,component: make QWeb an event bus
closes #197
2019-06-24 09:47:11 +02:00
Géry Debongnie 63a8fcd7e2 [FIX] component: scoping issue with t-on and t-foreach
closes #193
2019-06-21 16:31:24 +02:00
Géry Debongnie e6a5934162 [REF] component: use 'component' instead of 'widget' 2019-06-20 12:31:00 +02:00
Pierre Paridans 5524c2e323 [IMP] store: connected component reuse base component name
Connected component should have a unique name. To achieve this the
connected component currently have an id based generated name (like
`ConnectedComponent1`).

To make it clearer and ease debugging this commit reuse the extended
component's name to include it in the generated name in the form of
`Connected<ComponentName>`.
2019-06-20 12:26:13 +02:00
Géry Debongnie 35c1de26b8 [IMP] component: allow component name in templates
closes #186
2019-06-18 15:02:35 +02:00
Géry Debongnie d1cf6b1b8d [FIX] playground: sample was using old t-foreach syntax 2019-06-17 16:14:49 +02:00
Géry Debongnie bfa2c681cd [ADD] tools: add v0.15.0 benchmark 2019-06-17 16:07:06 +02:00
Géry Debongnie 609c108607 [REL] bump to v0.15.0 2019-06-17 15:59:15 +02:00
Aaron Bohy a56939f13a [IMP] store: memoize getters
Closes #95
2019-06-17 15:46:48 +02:00
Géry Debongnie 5ff501f8b8 [IMP] component: add support for default slot
closes #180
2019-06-17 09:26:37 +02:00
Géry Debongnie 60917bb102 [DOC] improve main readme.md 2019-06-15 10:38:29 +02:00
Géry Debongnie b201ef8162 [FIX] component: crashes if no template can be found
closes #178
2019-06-14 23:33:45 +02:00
Géry Debongnie 9cacc78ad7 [DOC] component: add section on root widget 2019-06-14 23:11:23 +02:00
Géry Debongnie 0d933b8570 [IMP] tools: improve playground samples
closes #177
2019-06-14 21:44:12 +02:00
Aaron Bohy 72f6daa695 [FIX] playground: t-async leftover in sample 2019-06-14 21:03:21 +02:00
Géry Debongnie 567d78a9ac [DOC] component: put input bindings section higher 2019-06-14 15:46:14 +02:00
Géry Debongnie f013c57050 [REF] component: rename t-async into t-asyncroot 2019-06-14 15:40:32 +02:00
Géry Debongnie b9fae87273 [DOC] doc: miscellaneous formatting updates 2019-06-14 15:33:43 +02:00
Géry Debongnie 1f0457f69b [REF] tools: use t-model in benchmarks 2019-06-14 15:01:29 +02:00
Géry Debongnie c481a73a76 [ADD] component: implement t-model directive
closes #170
2019-06-14 15:01:29 +02:00
Aaron Bohy e64e415b3b [IMP] qweb: add t-async directive on t-widget
Closes #98
2019-06-14 14:58:24 +02:00
Géry Debongnie aa406b53e2 [FIX] tools: react benchmarks did not look like the others 2019-06-14 09:36:29 +02:00
Géry Debongnie 57027bd0fa [FIX] doc: add missing link 2019-06-13 16:02:35 +02:00
Aaron Bohy 671c662fee [FIX] playground: don't remove empty tabs
Fixes #169
2019-06-13 14:04:59 +02:00
Géry Debongnie 0ae70b1d63 [ADD] tools: add v0.14.0 benchmark 2019-06-13 12:22:38 +02:00
Géry Debongnie 2701a7d861 [REL] bump to v0.14.0 2019-06-13 12:10:52 +02:00
Aaron Bohy 3ebe985c3c [FIX] qweb: no more duplicated nodes during transitions
If a node was re-added while being removed (i.e. during the remove
transition), the node was duplicated in the DOM for the delay of
the remove transition. This rev. removes the old occurence directly
in that case.

Fixes #121
2019-06-13 11:55:10 +02:00
Aaron Bohy 1c0d4b5832 [FIX] tests: update snapshots 2019-06-13 11:55:10 +02:00
Géry Debongnie 2840794fa2 [IMP] tools: rework benchmarks application
closes #130
2019-06-13 11:41:49 +02:00
Géry Debongnie 710f42d4e4 [IMP] qweb: add t-slot directive
Closes #67
2019-06-12 14:54:24 +02:00
Géry Debongnie 45a2b0122d [FIX] qweb: add better support for template with only strings 2019-06-12 14:54:24 +02:00
Géry Debongnie 4c41f62364 [FIX] owl: remove inline source maps
They are wrong anyway. Simpler to work with the raw javascript.
2019-06-12 14:54:24 +02:00
Aaron Bohy c2ab9774fb [IMP] utils: whenReady now returns a Promise
Closes #160
2019-06-11 14:45:56 +02:00
Aaron Bohy 22e48e3be0 [REF] store: simplify connect API
Closes #158
2019-06-11 11:41:29 +02:00
Aaron Bohy 1ff3c32fe4 [IMP] store: allow connecting to another store
Closes #58
2019-06-11 11:41:29 +02:00
Géry Debongnie 9a3e1ed99e [FIX] benchmarks: two small fixes 2019-06-11 11:29:31 +02:00
Géry Debongnie 438e2fbc2e [FIX] qweb: add some missing operators
closes #162
2019-06-11 10:45:20 +02:00
Géry Debongnie b3db085745 [REF] vdom: simplify vnode function
This is a snabbdom commit
2019-06-11 10:23:57 +02:00
Géry Debongnie b948c59c6d [DOC] *: improve some parts of the doc 2019-06-11 10:23:46 +02:00
Géry Debongnie 6426fe98f9 [DOC] tooling: improve doc 2019-06-10 09:31:06 +02:00
Géry Debongnie 4dd9de349e [FIX] tests: check links for main README.md as well 2019-06-10 09:31:06 +02:00
Géry Debongnie e1bcea77b5 [REF] tools: rename extras into tools 2019-06-10 09:31:06 +02:00
Géry Debongnie 2a19aeb6a3 [REF] extras: start loading templates earlier 2019-06-08 16:29:01 +02:00
Géry Debongnie b53a34b7d1 [FIX] utils: whenReady should work for all document states
Document readyState can be either 'loading', 'interactive' or
'complete'. We can access the DOM whenever we are no longer in 'loading'
state.
2019-06-08 16:20:44 +02:00
Géry Debongnie f881640d3d [DOC] event_bus: add initial doc
closes #157
2019-06-07 16:07:41 +02:00
Géry Debongnie 54c3dd76e4 [FIX] doc: fix all dead links
closes #149
2019-06-07 15:40:03 +02:00
Géry Debongnie 7b887447da [IMP] qweb: implements an expression evaluator
closes #5
2019-06-07 15:37:45 +02:00
Géry Debongnie 438b21df3f [REF] qweb: improve variable handling 2019-06-07 15:37:45 +02:00
Aaron Bohy d1094f647a [IMP] observer: add set and delete static functions
This rev. add a 'set' and 'delete' static functions to the Observer.
They allow to set or delete (only for objects) properties on
observed objects or arrays.

With this, the 'set' functions in Store and Component are no longer
necessary, so they have been removed.

Closes #138
2019-06-07 14:55:12 +02:00
Aaron Bohy 49baf0de6e [REF] qweb: t-foreach: remove integer support for t-value
Handling it complexifies the compiled code, and it is not really
useful (the same result can be achieved with t-foreach="Array(n)")

Part of #128
2019-06-07 14:52:12 +02:00
Aaron Bohy b14d069ee7 [REF] qweb: drop support of #{.} string interpolation
We only support the {{.}} syntax.

Part of #128
2019-06-07 14:52:12 +02:00
Aaron Bohy e14a4fe338 [REF] qweb: t-foreach: remove x_parity
Part of #128
2019-06-07 14:52:12 +02:00
Aaron Bohy 1f9ec46236 [FIX] qweb: keep ref of unmounted (kept alive) widget
Fixes #120
2019-06-06 09:11:52 +02:00
Géry Debongnie 1464a3631b [ADD] benchmarks: add 0.13.0 to the list 2019-06-05 11:24:27 +02:00
Géry Debongnie 7d3c374b78 [REL] bump to v0.13.0 2019-06-05 10:47:39 +02:00
Géry Debongnie 1dd257f33c [DOC] owl: add comment on updating the doc 2019-06-05 10:44:58 +02:00
Vincent Schippefilt 1206afe6be [FIX] base: compile correctly on windows
Fix package.json and tsconfig.json to build correctly on windows:
the extention scr/* doesn't expand on windows, so I removed it.

Added a precompilation step on npm run extras:watch
2019-06-05 10:31:18 +02:00
Vincent Schippefilt 5842ed51b0 [IMP] extras: update server to python3, disable cache 2019-06-05 10:31:18 +02:00
Géry Debongnie d22219f084 [DOC] reorganize qweb/component doc
Also, document t-foreach

closes #146
2019-06-05 10:28:30 +02:00
Aaron Bohy 0be1a54e03 [IMP] playground: use custom events bubbling 2019-06-04 11:59:21 +02:00
Aaron Bohy 32921ecc20 [IMP] qweb: t-on with t-widget: allow to bind args 2019-06-04 11:59:21 +02:00
Aaron Bohy 1c290c1172 [IMP] qweb: handle t-on modifiers with t-widget 2019-06-04 11:59:21 +02:00
Aaron Bohy 2f9d7ea58f [REF] qweb: revamp event management
Closes #111
2019-06-04 11:59:21 +02:00
Géry Debongnie fe23e76341 [FIX] observer: allow reobserving values twice
closes #139
2019-06-04 11:18:12 +02:00
Géry Debongnie 73023d4869 [IMP] extras: add url in log when starting server 2019-06-04 10:25:50 +02:00
Géry Debongnie ff51443b8e [FIX] playground: keep ace editor alive between mounting/remounting
closes #136
2019-06-04 09:43:58 +02:00
Géry Debongnie f8e07f6d30 [DOC] *: remove all remaining references to t-props
closes #135
2019-06-03 13:34:01 +02:00
Vincent Schippefilt 00da778753 [FIX] extras remove references to t-props 2019-06-03 13:34:01 +02:00
Géry Debongnie 187eb922c0 [IMP] component: define props as attribute (remove t-props) 2019-06-03 13:34:01 +02:00
Géry Debongnie 466c12a0e6 [FIX] component: reuse widget if possible
Previous code always destroy and recreate widgets.
2019-06-03 13:34:01 +02:00
Géry Debongnie 8dc3ec94bf [IMP] qweb: log invalid code generated by template 2019-05-31 13:56:45 +02:00
Géry Debongnie a554436d03 [FIX] playground: properly resize editors when resizing panels 2019-05-31 13:11:08 +02:00
Géry Debongnie 34b5049dee [DOC] doc: small misc fixes 2019-05-31 12:40:02 +02:00
Géry Debongnie 0eccfd08fe [REF] component: add some docstrings 2019-05-31 10:26:37 +02:00
Vincent Schippefilt 98b063fd70 [FIX] owl: ignore idea ide configuration 2019-05-31 09:51:37 +02:00
Géry Debongnie e9ba94ae55 [IMP] playground: add some padding in error window
closes #134
2019-05-29 16:13:29 +02:00
Aaron Bohy 4d72a4f240 [FIX] qweb: enclose interpolated expr in parentheses
Since rev 53e53e9b,
 <div class="o_app" t-attf-class="{{ true ? 'o_extra' : '' }}"/>
produced
 <div class="o_extra"/>
whereas it should have produced
 <div class="o_app o_extra"/>

This is because there were missing parentheses around the string
returned by `interpolate`, which produced instructions like

 var _1 = 'o_app ' + true ? 'o_extra' : '';

Fixes #132
2019-05-29 13:31:48 +02:00
Géry Debongnie a08deb895d [ADD] benchmarks: add 0.12.0 to the list 2019-05-29 13:24:58 +02:00
Géry Debongnie 60cea14b01 [IMP] extras: improve landing page 2019-05-29 13:16:35 +02:00
177 changed files with 44569 additions and 10223 deletions
+6 -1
View File
@@ -1,5 +1,6 @@
/node_modules
/dist
npm-debug.log
# misc
@@ -14,8 +15,12 @@ yarn-debug.log*
yarn-error.log*
package-lock.json
#ide's
.vscode
.idea
node_modules
# Extras temp file
/extras/owl.js
/tools/owl.js
+43 -28
View File
@@ -4,16 +4,20 @@ _A 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 small (~16kb gzipped) 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 features are:
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
- a store implementation (for state management)
- a small frontend router
**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.
## OWL's design principles
## OWL's 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,16 +30,27 @@ 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
## Example
Here is a short example to illustrate interactive widgets:
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 ClickCounter extends owl.Component {
class Counter extends owl.Component {
state = { value: 0 };
increment() {
@@ -43,36 +58,33 @@ class ClickCounter extends owl.Component {
}
}
const TEMPLATES = `
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb });
const counter = new Counter({ qweb });
counter.mount(document.body);
```
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
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.12.0.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.js)
- [owl-0.12.0.min.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.min.js)
- [owl-0.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
- [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.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 extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
| `npm run extras:watch` | same as `extras`, but with a watcher to rebuild owl |
| 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
@@ -82,6 +94,9 @@ The complete documentation can be found [here](doc/readme.md). The most importan
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## License
OWL is [GPL licensed](./LICENSE).
+59 -7
View File
@@ -1,16 +1,14 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<a class="btn flash" t-on-click="doSomething">Click</a>
```
@@ -19,7 +17,7 @@ with the following CSS:
```css
btn {
background-color: gray;
background-color: gray;
}
.flash {
@@ -35,10 +33,64 @@ btn {
will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button.
## CSS Transitions (single element)
## CSS Transitions
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us (see [QWeb documentation](qweb.md#t-transition-directive)).
The `t-transition` directive is here to help us. It works on html elements and
on components, by adding and removing some css classes.
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
The `t-transition` directive can be applied on a node element or on a component.
Notes:
- more information on animations are available [here](animations.md).
- Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed)
+64 -22
View File
@@ -7,26 +7,40 @@ In this page, we try to highlight some of these differences. Obviously, some
effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
## Content
- [Size](#size)
- [Class Based](#class-based)
- [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering)
- [Reactiveness](#reactiveness)
- [State Management](#state-management)
### Size
## Size
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 |
### Tooling/Build step
## 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
OWL is designed to be easy to use in a standalone way. For various reasons,
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
@@ -43,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
@@ -84,22 +98,22 @@ This has the advantage of having the full power of Javascript, but is less
structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
### Asynchronous rendering
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
- `willStart` (before the widget starts rendering)
- `willStart` (before the component starts rendering)
- `willUpdateProps` (before new props are set)
Both these methods can be implemented and return a promise. The rendering will
then wait for these promises to be completed before patching the DOM. This is
useful for some use cases: for example, a widget may want to fetch an external
library (a calendar widget may need a specialized calendar rendering library),
useful for some use cases: for example, a component may want to fetch an external
library (a calendar component may need a specialized calendar rendering library),
in its willStart hook.
```javascript
class MyCalendarWidget extends owl.Component {
class MyCalendarComponent extends owl.Component {
...
willStart() {
@@ -115,7 +129,7 @@ extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
more convoluted.
### Reactiveness
## Reactiveness
React has a simple model: whenever the state changes, it is
replaced with a new state (via the setState method). Then, the DOM is patched.
@@ -126,10 +140,12 @@ 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
## State Management
Managing the state of an application is a tricky issue. Many solutions have
been proposed these last few years. It also depends on the kind of application we
@@ -188,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 });
```
+645 -107
View File
@@ -4,15 +4,24 @@
- [Overview](#overview)
- [Example](#example)
- [Composition](#composition)
- [Reference](#reference)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Root Component](#root-component)
- [Environment](#environment)
- [Composition](#composition)
- [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)
- [Asynchronous rendering](#asynchronous-rendering)
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
## Overview
@@ -21,11 +30,11 @@ OWL components are the building blocks for user interface. They are designed to
1. **declarative:** the user interface should be described in term of the state
of the application, not as a sequence of imperative steps.
2. **composable:** each widget can seamlessly be created in a parent widget by
a simple directive in its template.
2. **composable:** each component can seamlessly be created in a parent component by
a simple tag or directive in its template.
3. **asynchronous rendering:** the framework will transparently wait for each
subwidgets to be ready before applying the rendering. It uses native promises
sub components to be ready before applying the rendering. It uses native promises
under the hood.
4. **uses QWeb as a template system:** the templates are described in XML
@@ -34,7 +43,7 @@ OWL components are the building blocks for user interface. They are designed to
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way.
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).
@@ -69,58 +78,9 @@ 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.
## Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
```xml
<div t-name="ParentWidget">
<span>some text</span>
<t t-widget="MyWidget" t-props="{info: 13}"/>
</div>
```
```js
class ParentWidget extends owl.Component {
widgets = { MyWidget: MyWidget};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. See the [QWeb](qweb.md) documentation for more information on the
`t-widget` directive.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element.
```xml
<div t-name="ParentWidget">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
## Reference
An Owl component is a small class which represent a widget or some UI element.
An Owl component is a small class which represent a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template.
@@ -141,7 +101,7 @@ find a template with the component name (or one of its ancestor).
- **`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 widget to rerender itself.
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,
@@ -149,7 +109,7 @@ find a template with the component name (or one of its ancestor).
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 widgets defined by a `t-ref` directive in the component's template.
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties
@@ -157,17 +117,43 @@ find a template with the component name (or one of its ancestor).
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.
- **`defaultProps`** (Object, optional): if given, this object define default
```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
- **`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.
We explain here all the public methods of the `Component` class.
- **`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
@@ -181,7 +167,7 @@ find a template with the component name (or one of its ancestor).
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the widget should
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
@@ -192,32 +178,33 @@ find a template with the component name (or one of its ancestor).
if we have a `isMobile` key in the environment, to decide if we want a mobile
interface or a destkop one.
- **`set(target, key, value)`**. This method is necessary in some cases when we
need to modify the state of the component in a way that is not visible to the
observer (see [observer's technical limitations](observer.md#technical-limitations)).
For example, if we need to add a key to the state.
- **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
called directly (except maybe on the root component), but should be done by the
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
starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change.
### Lifecycle
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](#constructor)** | 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:
@@ -262,7 +249,7 @@ perform some action before the initial rendering of a component.
It will be called exactly once before the initial rendering. It is useful
in some cases, for example, to load external assets (such as a JS library)
before the widget is rendered. Another use case is to load data from a server.
before the component is rendered. Another use case is to load data from a server.
```javascript
async willStart() {
@@ -274,7 +261,8 @@ 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 widget rendering will take place after `willStart` is completed.
After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()`
@@ -319,8 +307,8 @@ scrollbar.
Note that modifying the state object 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 widget is not in the DOM (this can
happen with widgets with `t-keepalive`).
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.
@@ -332,12 +320,12 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the widget is
not in the DOM (this can happen with widgets with `t-keepalive`).
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 widget state in this hook is possible, but not encouraged.
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
careful at avoiding endless cycles.
@@ -358,6 +346,377 @@ 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`
directive) in a template. There is however an obvious exception: the root component
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root component needs an environment.
### Environment
In Owl, an environment is an object with a `qweb` key, which has to be a
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
The environment is meant to contain (mostly) static global information and
methods for the whole application. For example, settings keys (`mode` to determine
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
session information, ...
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
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:
```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);
}
```
### Composition
The example above shows a QWeb template with a sub component. In a template,
components are declared with a tagname corresponding to the class name. It has
to be capitalized.
```xml
<div t-name="ParentComponent">
<span>some text</span>
<MyComponent info="13" />
</div>
```
```js
class ParentComponent extends owl.Component {
components = { MyComponent: MyComponent};
...
}
```
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
after the span. The `info` key will be added to the subcomponent's `props`. Each
`props` is a string which represents a javascript (QWeb) expression, so it is
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.
```xml
<div t-name="ParentComponent">
<ChildComponent count="state.val" />
</div>
```
```js
class ParentComponent {
components = { ChildComponent };
state = { 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
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `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
on the component (and also, in the template). Whenever the state is updated, then
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):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
**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.
```xml
<div t-name="ParentComponent">
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Event Handling
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<MyComponent t-on-menu-loaded="someMethod" />
```
```js
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element
(`this.el`). The event bubbles and is cancelable. The parent component listening
to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered.
```js
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
The `t-on` directive also allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Form Input Bindings
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = { text: "" };
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
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:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="text" />
<span t-esc="state.text" />
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
<div>Selection:
<select t-model="color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts some modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | tries to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### `t-key` Directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
There are three main use cases:
- _elements in a list_:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text" />
</span>
```
- _`t-if`/`t-else`_
- _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
@@ -381,27 +740,27 @@ component (with some code like `app.mount(document.body)`).
2. when it is done, template `A` is rendered.
- widget `B` is created
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- widget `C` is created
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- widget `D` is created
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- widget `E` is created
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. widget `A` is patched into a detached DOM element. This will create the actual
widget `A` DOM structure. The patching process will cause recursively the
3. component `A` is patched into a detached DOM element. This will create the actual
component `A` DOM structure. The patching process will cause recursively the
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
in one pass)
4. the widget `A` root element is actually appended to `document.body`
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all widgets in the following
5. The method `mounted` is called recursively on all components in the following
order: `B`, `D`, `E`, `C`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
@@ -409,7 +768,7 @@ button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new widget `F`.
- add new component `F`.
So, the component tree should look like this:
@@ -426,17 +785,17 @@ Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- widget `D` is updated:
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- widget `F` is created:
- component `F` is created:
1. hook `willStart` is called on `E` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`,
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. widget `C` is patched, which will cause recursively:
4. component `C` is patched, which will cause recursively:
2. `willUnmount` hook on `E`, then destruction of `E`,
3. (initial) patching of `F`, then hook `mounted` is called on `F`
@@ -460,7 +819,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:
@@ -488,15 +848,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
@@ -514,6 +875,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
@@ -528,11 +894,108 @@ 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
};
```
## Asynchronous rendering
### 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:
```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
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someComponent;
```
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,
```xml
<div t-ref="component_{{id}}"/>
```
```js
this.refs.component_44;
```
### Slots
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
This is what _slots_ are for.
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
<div class="modal-content">
<t t-slot="content"/>
</div>
<div class="modal-footer">
<t t-slot="footer"/>
</div>
</div>
```
Slots are defined by the caller, with the `t-set` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<div>hey</div>
</t>
<t t-set="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
</div>
```
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Default slot: the first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
@@ -541,18 +1004,93 @@ components.
There are two different common problems with Owl asynchronous rendering model:
- any widget can delay the rendering (initial and subsequent) of the whole
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given widget, there are two independant situations that will trigger an
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous widgets:
Here are a few tips on how to work with asynchronous components:
1. minimize the use of asynchronous widgets!
1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`)
4. For all the other cases, the `t-asyncroot` directive (to use alongside
`t-component`) is there to help you. When this directive is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<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 Widget {
state = { 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
});
```
+27
View File
@@ -0,0 +1,27 @@
# 🦉 Event Bus 🦉
It is sometimes useful to use a `Bus` to communicate informations between various
parts of the code. Owl has a very simple bus class, which manages subscriptions,
triggering events, and callbacks.
```js
const bus = new owl.EventBus();
bus.on("some-event", null, function(...args) {
console.log(...args);
});
bus.trigger("some-event", 1, 2, 3);
// [1,2,3] will be logged to the console
```
Its API is:
| Method | Description |
| -------------------------------- | --------------------------------- |
| `on(eventType, owner, callback)` | add a listener |
| `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions |
Note that the [`Store`](store.md) is an example of an `EventBus`.
+23 -27
View File
@@ -20,35 +20,31 @@ obj.a.b = 2;
## Technical Limitations
Since the observer uses getters and setters, it is actually unable to react to
changes in two situations:
changes in three situations:
- adding a key to an object:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // will do nothing
```
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`.
In that case, we need a way to tell the observer that something happened.
This can be done by using the `set` method:
```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
```javascript
observer.set(obj, "b", 2);
```
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
- modifying an array by setting a new value at a given index:
```javascript
const observer = new owl.Observer();
const obj = { todos: [{ id: 1, text: "todo" }] };
observer.observe(obj);
obj[0] = { id: 2, text: "othertodo" }; // will do nothing, and obj[0] is not observed
```
In that case, the solution is the same, we can simply use the `set` method:
```javascript
observer.set(obj, 0, { id: 2, text: "othertodo" });
```
```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
```
+5 -5
View File
@@ -1,6 +1,6 @@
# 🦉 Quick Start 🦉
## Static server
## Static Server
Let us assume that we have a static server running somewhere. We could then
simply add an html page with a few extra files.
@@ -58,16 +58,16 @@ To build an application (or a sub-part of an application), we need two things:
need. In practice, it could context some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode).
- a description of the user interface: there should be a root widget, which can
have sub widgets
- a description of the user interface: there should be a root component, which can
have sub components
Here are a few steps that we may take to get started:
- get the templates
- create a qweb engine, with the templates
- create an environment
- create an instance of the root widget
- mount the root widget to a DOM element
- create an instance of the root component
- mount the root component to a DOM element
Let us now add the javascript to make it work, in `app.js`:
+243 -401
View File
@@ -3,44 +3,78 @@
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [QWeb Specification](#qweb-specification)
- [Static html nodes](#static-html-nodes)
- [`t-esc` directive](#t-esc-directive)
- [`t-raw` directive](#t-raw-directive)
- [`t-set` directive](#t-set-directive)
- [`t-if` directive](#t-if-directive)
- [Expression evaluation](#expression-evaluation)
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
- [`t-on` directive](#t-on-directive)
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
- [`t-ref` directive](#t-ref-directive)
- [`t-key` directive](#t-key-directive)
- [`t-transition` directive](#t-transition-directive)
- [`t-mounted` directive](#t-mounted-directive)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
- [Reference](#reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Debugging](#debugging)
## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate html. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the html.
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
The QWeb implementation in the OWL project is slightly different. It compiles
templates into functions that output a virtual DOM instead of a string. This is
necessary for the component system. In addition, it has a few extra directives
(see [OWL Specific Extensions](#owlspecificextensions))
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message">
</li>
</ul>
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
## Directives
We present here a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-component`, `t-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) |
## QWeb Engine
@@ -61,12 +95,14 @@ It's API is quite simple:
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr)`**: add a specific template.
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
@@ -74,7 +110,7 @@ It's API is quite simple:
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherWidget">other widget</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
@@ -83,12 +119,19 @@ It's API is quite simple:
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", widget);
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`register(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-widget` directive). This is useful for commonly used
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
@@ -97,16 +140,107 @@ It's API is quite simple:
...
class ParentWidget extends owl.Component { ... }
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
## QWeb Specification
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsability: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered.
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### Static html nodes
### White Spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root Nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that applies on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context (typically, the component):
```xml
<p t-if="user.birthday === today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves:
@@ -114,7 +248,7 @@ Normal, regular html nodes are rendered into themselves:
<div>hello</div> <!–– rendered as itself ––>
```
### `t-esc` directive
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
@@ -129,14 +263,12 @@ rendered with the value `value` set to `42` in the rendering context yields:
<p>42</p>
```
### `t-raw` directive
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe.
```xml
<p><t t-esc="value"/></p>
<p><t t-raw="value"/></p>
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
@@ -145,7 +277,7 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
<p><span>foo</span></p>
```
### `t-set` directive
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
@@ -177,7 +309,7 @@ This is done via the `t-set` directive, which takes the name of the variable to
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### `t-if` directive
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
@@ -227,52 +359,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
</div>
```
### Expression evaluation
It is useful to explain the various rules that applies on QWeb expressions. These
expressions are strings that will be converted to a javascript expression at
compile time.
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context:
```xml
<p t-if="user.birthday == today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Dynamic attributes (`t-att` and `t-attf` directives)
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
@@ -292,19 +379,77 @@ If an expression evaluates to a falsy value, it will not be set at all:
There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation:
```xml
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
For historical reason, there is an alternate form of string interpolation:
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
### `t-call` directive (sub templates)
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
collection to iterate on, and a second parameter `t-as` providing the name to use
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-esc="i"/></p>
</t>
```
will be rendered as:
```xml
<p>1</p>
<p>2</p>
<p>3</p>
```
Like conditions, `t-foreach` applies to the element bearing the directives attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
`t-foreach` can iterate on an array (the current item will be the current value)
or an object (the current item will be the current key).
In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced by the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratees size be
available
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="False"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i">
<t t-set="existing_variable" t-value="True"/>
<t t-set="new_variable" t-value="True"/>
<!-- existing_variable and new_variable now True -->
</p>
<!-- existing_variable always True -->
<!-- new_variable undefined -->
```
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
@@ -352,276 +497,7 @@ will result in :
</div>
```
## JS/OWL Specific Extensions
### `t-on` directive
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This
is what makes a template _alive_. There are two different use cases.
1. Register an event handler on a DOM node
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", widget.someMethod.bind(widget));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
In order to remove the DOM event details from the event handlers (like calls
to `event.preventDefault`) and let them focus on data logic, _modifiers_ can
be specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the
element itself.
2. Register an event handler on a component. This will not capture a DOM event,
but rather a _business_ event:
```xml
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
```
```js
class MyWidget {
someWhere() {
const payload = ...;
this.trigger('menuLoaded', payload);
}
}
```
Here, the parent widget will receive the payload in its `someMethod` handler,
whenever the event is triggered.
The `t-on` directive also allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Component: `t-widget`, `t-props`
The `t-widget` and the `t-props` directives are the key to a declarative component
system. They allow a template to define where and how a sub widget is created
and/or updated. For example:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) directive):
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget#{id}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget1, ChildWidget2 };
state = { id: 1 };
}
```
Similarly to `t-attf-`, there is an alternate form of string interpolation:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget{{id}}"/>
</div>
```
### `t-ref` directive
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:
```xml
<div>
<div t-ref="someDiv"/>
<t t-widget="SubWidget" t-ref="someWidget"/>
</div>
```
In this example, the widget will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someWidget;
```
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 widget.
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-`](#dynamic-attributes-t-att-and-t-attf-directives) and
[`t-widget-`](#component-t-widget-t-props) directives). For example, if we have
`id` set to 44 in the rendering context,
```xml
<div t-ref="widget_#{id}"/>
```
```js
this.refs.widget_44;
```
Similarly to `t-attf-` and `t-widget`, there is an alternate form of string
interpolation:
```xml
<div t-ref="widget_{{id}}"/>
```
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
There are three main use cases:
- _elements in a list_:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/>
</span>
```
- _`t-if`/`t-else`_
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-transition` directive
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
The `t-transition` directive can be combined with `t-widget`.
Notes:
- more information on animations are available [here](animations.md).
- the value for `t-transition` must be a single class name. Owl does not support
more than one transition on a single node.
### `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 MyWidget extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Debugging (`t-debug` and `t-log`)
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
@@ -643,37 +519,3 @@ will stop execution if the browser dev tools are open.
```
will print 42 to the console
### White spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
+38 -6
View File
@@ -1,17 +1,49 @@
# 🦉 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
core
EventBus
Observer
router
Link
Router
store
Store
ConnectedComponent
utils
debounce
escape
loadJS
loadTemplates
whenReady
```
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [QWeb](qweb.md)
- [Store](store.md)
- [Event Bus](event_bus.md)
- [Observer](observer.md)
- [Virtual DOM](vdom.md)
- [QWeb](qweb.md)
- [Router](router.md)
- [Store](store.md)
- [Utils](utils.md)
- [Virtual DOM](vdom.md)
## Learning Resources
- [Quick Start](quick_start.md)
## Miscellaneous
- [Quick Start](quick_start.md)
- [Animations](animations.md)
- [Tooling](tooling.md)
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
+172
View File
@@ -0,0 +1,172 @@
# 🦉 Router 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [`t-routecomponent`](#t-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 `t-routecomponent` directive
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.
### `t-routecomponent`
The `t-routecomponent` directive directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<t t-routecomponent="1"/>
</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>
```
+204 -64
View File
@@ -5,11 +5,12 @@
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Public API](#public-api)
- [Mutations](#mutations)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a component](#connecting-a-component)
- [Connecting a Component](#connecting-a-component)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
@@ -20,88 +21,132 @@ 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
Here is what a simple store look like:
Here is what a simple store looks like:
```js
const actions = {
addTodo({commit}, message) {
commit('addTodo', message);
}
};
const mutations = {
addTodo({state}, message) {
const todo = {
id: state.nextId++,
message,
isCompleted: false,
};
state.todos.push(todo);
},
addTodo({ state }, message) {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false
});
}
};
const state = {
todos: [],
nextId: 1,
todos: [],
nextId: 1
};
const store = new owl.Store({state, actions, mutations});
store.on('update', () => console.log(store.state));
const store = new owl.Store({ state, actions });
store.on("update", () => console.log(store.state));
// updating the state
store.dispatch('addTodo', 'fix all bugs');
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` 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 a `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
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
### 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
```javascript
const config = {
state,
actions,
getters,
env
};
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}) {
commit('setLoginState', 'pending');
try {
const loginInfo = await doSomeRPC('/login/', 'someinfo');
commit('setLoginState', loginInfo);
} catch {
commit('setLoginState', 'error');
}
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo;
} catch {
state.loginState = "error";
}
}
};
```
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partial corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
}
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
}
};
```
@@ -128,22 +173,117 @@ transform the data contained in the store.
```js
const getters = {
getPost({state}, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => a.id = post.id);
return {
id,
author,
content: post.content
};
},
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => (a.id = post.id));
return {
id,
author,
content: post.content
};
}
};
// somewhere else
const post = store.getters.getPost(id);
```
### Connecting a component
Getters take _at most_ one argument.
Todo
Note that getters are cached if they don't take any argument, or their argument
is a string or a number.
### Connecting a Component
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The `ConnectedComponent` class can be configured with the following fields:
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict.
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)
Note that the class `ConnectedComponent` has a `dispatch` method. This means
that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
</button>
```
### Semantics
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children)
- they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+46 -6
View File
@@ -1,18 +1,58 @@
# 🦉 Tooling 🦉
## Development mode
## Content
By default, Owl is in *production* mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
- [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
## Overview
To help work with/improve/learn OWL, there are a few extras tools/settings.
- development mode: enable better error reporting for the developer
- a playground application: a space to experiment and learn Owl.
- a benchmarks application: allow comparison with a few common frameworks
The two applications are available in the `tools/` folder, and can be accessed
by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = 'dev';
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.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground
The playground is an important application designed to help learning and
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
It is an application similar to `jsFiddle`, but specialized for Owl: there are
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
code in an iframe.
## Benchmarks
Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks.
+43 -17
View File
@@ -5,35 +5,61 @@ functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready)
- [`loadJS`](#loadjs)
- [`loadTemplates`](#loadtemplates)
- [`escape`](#escape)
- [`debounce`](#debounce)
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadTemplates`](#loadtemplates): loading xml files
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
## `whenReady`
The function `whenReady` is useful to register some code that need to be executed
as soon as the document (page) is ready:
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
templates
]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
});
```
```js
owl.utils.whenReady(function() {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
});
```
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
a promise, so the caller can properly react when it is ready. Also, it is smart:
it maintains a list of urls previously loaded (or currently being loaded), and
prevent doing twice the work.
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
## `loadTemplates`
```js
async function makeEnv() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
return { qweb };
}
```
## `escape`
## `debounce`
+16 -1
View File
@@ -1,3 +1,18 @@
# 🦉 VDom 🦉
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
@@ -1,30 +0,0 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="message_count">
Number of msg: 0
</div>
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1000 messages</button>
<button class="o_btn_msg 10000">Add 10000 messages</button>
<button class="o_btn_msg 50000">Add 50000 messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove" >Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
@@ -1,30 +0,0 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="message_count">
Number of msg: 0
</div>
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1000 messages</button>
<button class="o_btn_msg 10000">Add 10000 messages</button>
<button class="o_btn_msg 50000">Add 50000 messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove" >Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
-84
View File
@@ -1,84 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
@@ -1,32 +0,0 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-84
View File
@@ -1,84 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
@@ -1,32 +0,0 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-85
View File
@@ -1,85 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure('remove message');
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
-32
View File
@@ -1,32 +0,0 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-84
View File
@@ -1,84 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
-32
View File
@@ -1,32 +0,0 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-84
View File
@@ -1,84 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
-32
View File
@@ -1,32 +0,0 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-81
View File
@@ -1,81 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("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);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// 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();
@@ -1,32 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-221
View File
@@ -1,221 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Counter Widget
//------------------------------------------------------------------------------
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
this.increment = this.increment.bind(this);
}
render() {
return React.createElement(
"div",
null,
React.createElement(
"button",
{
onClick: this.increment
},
"Value: ",
this.state.counter
)
);
}
increment() {
this.setState({
counter: this.state.counter + 1
});
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends React.Component {
constructor(props) {
super(props);
this.removeMessage = this.removeMessage.bind(this);
}
render() {
return React.createElement(
"div",
{
className: "message"
},
React.createElement(
"span",
{
className: "author"
},
this.props.message.author
),
React.createElement(
"span",
{
className: "msg"
},
this.props.message.msg
),
React.createElement(
"button",
{
className: "remove",
onClick: this.removeMessage
},
"Remove"
),
React.createElement(Counter, null)
);
}
removeMessage() {
this.props.removeCB(this.props.message.id);
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: []
};
this.removeMessage = this.removeMessage.bind(this);
}
render() {
const msgList = this.state.messages.map(m =>
React.createElement(Message, {
key: m.id,
message: m,
removeCB: this.removeMessage
})
);
return React.createElement(
"div",
{
className: "main"
},
React.createElement(
"div",
{
className: "left-thing"
},
React.createElement(
"div",
null,
"Number of msg: ",
this.state.messages.length
),
React.createElement(
"button",
{
onClick: _ => this.addMessages(100)
},
"Add 100 messages"
),
React.createElement(
"button",
{
onClick: _ => this.addMessages(1000)
},
"Add 1000 messages"
),
React.createElement(
"button",
{
onClick: _ => this.addMessages(10000)
},
"Add 10000 messages"
),
React.createElement(
"button",
{
onClick: _ => this.addMessages(50000)
},
"Add 50000 messages"
),
React.createElement(
"button",
{
onClick: _ => this.updateSomeMessages()
},
"Update every 10th messags"
),
React.createElement(
"button",
{
onClick: _ => this.clear()
},
"Clear"
)
),
React.createElement(
"div",
{
className: "right-thing"
},
React.createElement(
"div",
{
className: "content"
},
msgList
)
)
);
}
addMessages(n) {
startMeasure("add " + n);
const newMessages = this.state.messages.concat(buildData(n));
this.setState({
messages: newMessages
});
stopMeasure();
}
clear() {
startMeasure("clear");
this.setState({
messages: []
});
stopMeasure();
}
updateSomeMessages() {
startMeasure("update every 10th");
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
messages[i].author += "!!!";
}
this.forceUpdate();
stopMeasure();
}
removeMessage(id) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === id);
const messages = this.state.messages.slice();
messages.splice(index, 1);
this.setState({
messages
});
stopMeasure();
}
} //-----------------------------
// INIT
//-----------------------------
ReactDOM.render(
React.createElement(Main, null),
document.getElementById("main")
);
-42
View File
@@ -1,42 +0,0 @@
.main {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: grid;
grid-template-columns: 220px 1fr;
}
.left-thing {
background-color: gray;
padding: 20px;
color: white;
}
.left-thing button {
width: 100%;
}
.right-thing {
padding: 20px;
overflow: auto;
}
/* Message widget */
.message .author {
font-weight: bold;
}
.message {
width: 400px;
background-color: lightblue;
margin: 10px 5px;
border-radius: 5px;
padding: 5px;
}
.remove {
float: right;
}
-106
View File
@@ -1,106 +0,0 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Counter Widget
//------------------------------------------------------------------------------
Vue.component("likes-counter", {
data: function() {
return {
counter: 0
};
},
methods: {
increment() {
this.counter++;
}
},
template: `
<div>
<button @click="increment">Value: {{counter}}</button>
</div>`
});
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
Vue.component("my-message", {
props: ["msg"],
methods: {
removeMessage() {
this.$emit("removeMessage", this.msg.id);
}
},
template: `
<div class="message">
<span class="author">{{msg.author}}</span>
<span class="msg">{{msg.msg}}</span>
<button class="remove" @click="removeMessage">Remove</button>
<likes-counter></likes-counter>
</div>`
});
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
const App = {
name: "App",
data() {
return { messages: [] };
},
methods: {
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.messages.push.apply(this.messages, newMessages);
stopMeasure();
},
clear() {
startMeasure("clear");
this.messages = [];
stopMeasure();
},
updateSomeMessages() {
startMeasure("update every 10th");
const messages = this.messages;
for (let i = 0; i < this.messages.length; i += 10) {
messages[i].author += "!!!";
}
stopMeasure();
},
removeMessage(id) {
startMeasure("remove message");
const index = this.messages.findIndex(m => m.id === id);
this.messages.splice(index, 1);
stopMeasure();
}
},
template: `
<div class="main">
<div class="left-thing">
<div>Number of msg: {{messages.length}}</div>
<button @click="addMessages(100)">Add 100 messages</button>
<button @click="addMessages(1000)">Add 1000 messages</button>
<button @click="addMessages(10000)">Add 10000 messages</button>
<button @click="addMessages(50000)">Add 50000 messages</button>
<button @click="updateSomeMessages">Update every 10th message</button>
<button @click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<my-message v-for="msg in messages" v-bind:key="msg.id" v-bind:msg="msg" @removeMessage="removeMessage"></my-message>
</div>
</div>
</div>
`
};
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
new Vue({
render: h => h(App)
}).$mount(`#main`);
-43
View File
@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL Extras</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="title">
<h1>🦉 OWL Extra Stuff 🦉</h1>
<div>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</div>
</div>
<div class="section misc">
<h2>Misc</h2>
<div class="links">
<a href="playground">Playground (local)</a>
<a href="https://github.com/odoo/owl">Github</a>
<a href="https://odoo.github.io/owl/">Owl main page</a>
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Owl Documentation</a>
</div>
</div>
<div class="section benchmarks">
<h2>Benchmarks</h2>
<ul>
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
<li><a href="benchmarks/vue">Vue</a></li>
<li><a href="benchmarks/react">React</a></li>
</ul>
</div>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
body {
font-family: sans-serif;
}
.title, .section {
width: 768px;
margin: auto;
}
.title div {
color: #333333;
font-style: italic;
}
.section {
background-color: #d5d5d5;
padding: 16px;
margin-top: 10px;
margin-bottom: 10px;
}
.links a {
margin: 10px 5px;
}
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
# 🦉 Extra Stuff 🦉
To help work/improve/learn with OWL, we have here:
- a benchmarks application
- a playground application
Both of them can be accessed by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run extras` (and its version with a watcher: `npm run extras:watch`).
## Benchmarks
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.
## Playground
The playground is an important application designed to help learning and
experimenting with Owl. It is available [online](https://odoo.github.io/owl/playground/).
-38
View File
@@ -1,38 +0,0 @@
import sys
import thread
import webbrowser
import time
import BaseHTTPServer
import SimpleHTTPServer
HOST = '127.0.0.1'
PORT = 8000
URL = 'http://{0}:{1}/extras'.format(HOST, PORT)
# We define our own handler here to remap owl.js GET requests to the Owl build
# in dist/. This is useful for the benchmarks and playground applications.
# With this, we can simply copy the playground folder as is in the gh-page when
# we want to update the playground.
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/extras/owl.js':
self.path = '/dist/owl.js'
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def start_server():
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
httpd.serve_forever()
if __name__ == "__main__":
thread.start_new_thread(start_server, ())
webbrowser.open_new(URL)
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
+12 -7
View File
@@ -1,18 +1,20 @@
{
"name": "owl",
"version": "0.12.0",
"version": "0.19.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
"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",
"extras:serve": "python extras/server.py",
"extras": "npm run build && npm run extras:serve",
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
"tools:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
},
"repository": {
"type": "git",
@@ -29,6 +31,7 @@
"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",
@@ -37,8 +40,7 @@
"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": {
@@ -59,5 +61,8 @@
"json",
"node"
]
},
"prettier": {
"printWidth": 100
}
}
-610
View File
@@ -1,610 +0,0 @@
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component)
* - the Component class
*/
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
export interface Env {
qweb: QWeb;
}
export interface Meta<T extends Env, Props> {
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 widgetID
// should it be a map number => Widget?
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 };
}
// 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 } } = {};
//------------------------------------------------------------------------------
// Widget
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
readonly __owl__: Meta<Env, Props>;
template?: string;
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
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
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* The root widget of a component tree needs an environment:
*
* ```javascript
* const root = new RootWidget(env, props);
* ```
*
* Every other widget simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeWidget(parent, props);
* ```
*
* Note that most of the time, only the root widget needs to be created by
* hand. Other widgets should be created automatically by the framework (with
* the t-widget directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
const defaultProps = (<any>this.constructor).defaultProps;
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 widget
// Con: this is not really safe
// Pro: but creating widget (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
} else {
this.env = parent;
}
this.__owl__ = {
id: id,
vnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {},
mountedHandlers: {}
};
}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the widget is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
* fast as possible.
*
* Note: this method should not be called manually.
*/
async willStart() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the widget state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
async mount(target: HTMLElement): Promise<void> {
const vnode = await this._prepare();
if (this.__owl__.isDestroyed) {
// widget was destroyed before we get here...
return;
}
this._patch(vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this._callMounted();
}
}
_callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
}
}
}
unmount() {
if (this.__owl__.isMounted) {
this._callWillUnmount();
this.el!.remove();
}
}
async render(force: boolean = false, patchQueue?: any[]): 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;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
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]);
}
}
}
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this._destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
_destroy(parent) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key]._destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
this.clear();
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* This method is the correct way to update the environment of a widget. Doing
* this will cause a full rerender of the widget 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);
}
}
set(target: any, key: string | number, value: any) {
this.__owl__.observer!.set(target, key, value);
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
async _updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[]
): Promise<void> {
const shouldUpdate = forceUpdate || 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);
}
}
_patch(vnode) {
const __owl__ = this.__owl__;
__owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
_prepare(): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this._prepareAndRender();
return __owl__.renderPromise;
}
async _prepareAndRender(): Promise<VNode> {
await this.willStart();
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;
} else {
while (
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
p = p.__proto__;
}
if (p === Component) {
this.template = "default";
} else {
tmap[name] = template;
this.template = template;
}
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
this._observeState();
return this._render();
}
async _render(
force: boolean = false,
patchQueue: any[] = []
): 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);
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child widget can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget 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;
}
/**
* Only called by qweb t-widget directive
*/
_mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this._callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-widget directive (when t-keepalive is set)
*/
_remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
_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).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
/**
* 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]}' (widget '${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}' (widget '${this.constructor.name}')`
);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in widget '${
this.constructor.name
}'`
);
}
}
}
}
}
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Check if an invidual prop value matches its (static) prop definition
*/
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;
}
// 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;
}
+725
View File
@@ -0,0 +1,725 @@
import { Observer } from "../core/observer";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import "./directive";
import "./props_validation";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component)
* - the Component class
*/
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env {
qweb: QWeb;
[key: string]: any;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
vnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, 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;
// the renderProps and renderPromise keys are only useful for the "prepare"
// step of the lifecycle of a component. Once a component has been rendered
// and patched, it is no longer useful.
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function };
classObj: { [key: string]: boolean } | 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 } } = {};
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> {
readonly __owl__: Internal<Env, Props>;
template?: string;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
env: T;
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
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
} else {
this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
}
this.__owl__ = {
id: id,
vnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {},
mountedHandlers: {},
observer: null,
render: null,
classObj: null
};
}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the component is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
* fast as possible.
*
* Note: this method should not be called manually.
*/
async willStart() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return;
}
if (!__owl__.vnode) {
const vnode = await this.__prepare();
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
const patchQueue = [];
await this.__render(false, patchQueue, undefined, undefined);
this.__applyPatchQueue(<any[]>patchQueue);
}
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.isMounted) {
this.__callWillUnmount();
this.el!.remove();
}
}
/**
* The render method is the main entry point to render a component (once it
* is ready. This method is not initially called when the component is
* rendered the first time).
*
* This method will cause all its sub components to potentially rerender
* themselves. Note that `render` is not called if a component is updated via
* its props.
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
return;
}
const patchQueue = [];
const renderId = ++__owl__.renderId;
await this.__render(force, patchQueue, undefined, undefined);
if (__owl__.isMounted && renderId === __owl__.renderId) {
// 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);
}
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* 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
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* 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, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp.__callMounted();
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
}
__callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp.__callWillUnmount();
}
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[],
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.__render(forceUpdate, patchQueue, scope, vars);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise;
}
async __prepareAndRender(scope?: Object, vars?: any): 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;
} else {
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;
}
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState();
return this.__render(false, [], scope, vars);
}
__render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__;
const promises: Promise<void>[] = [];
const patch: any[] = [this];
patchQueue.push(patch);
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode);
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child component can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
// we applly here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode);
}
/**
* Only called by qweb t-component directive
*/
__mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 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
*/
__applyPatchQueue(patchQueue: any[]) {
let component = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
component = patch[0];
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];
component = patch[0];
patch[0].patched(patch[2]);
}
} catch (e) {
errorHandler(e, component);
}
}
}
//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, component: Component<any, any, any>) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent!;
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
}
}
+501
View File
@@ -0,0 +1,501 @@
import { QWeb } from "../qweb/index";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive", "asyncroot"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
}
}
}
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`);
}
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? `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}"`;
}
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
if (classAttr) {
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.inLoop) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
}
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
);
if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
}
ctx.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${componentID}`);
// new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
ctx.addLine(
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
if (clone.childNodes.length) {
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
let scopeVars = "";
if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
if (varDefs.length) {
scopeVars += `, {${varDefs.join(",")}}`;
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(${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}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update component
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
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 ? ${patchQueueCode} : []`;
}
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}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
+96
View File
@@ -0,0 +1,96 @@
import { QWeb } from "../qweb/index";
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
QWeb.utils.validateProps = function(Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!props[propName]) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
}
}
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (propDef === true) {
return true;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
+1 -1
View File
@@ -29,7 +29,7 @@ export class EventBus {
* Add a listener for the 'eventType' events.
*
* Note that the 'owner' of this event can be anything, but will more likely
* be a widget or a class. The idea is that the callback will be called with
* be a component or a class. The idea is that the callback will be called with
* the proper owner bound.
*
* Also, the owner should be kind of unique. This will be used to remove the
+110
View File
@@ -0,0 +1,110 @@
/**
* 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(value: any, parent?: any): any {
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);
}
}
+15 -14
View File
@@ -4,19 +4,21 @@
*
* 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";
// 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 { QWeb };
export { connect, Store } from "./store";
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 { Link } from "./router/Link";
import { Router } from "./router/Router";
export { Component } from "./component/component";
export { QWeb };
export const core = { EventBus, Observer };
export const router = { Router, Link };
export const store = { Store, ConnectedComponent };
export const utils = _utils;
export const __info__ = {};
@@ -32,8 +34,7 @@ Object.defineProperty(__info__, "mode", {
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`)
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
export const utils = _utils;
-167
View File
@@ -1,167 +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;
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) {
this.rev++;
this._addProp(target, key, value);
target.__owl__.rev++;
this.notifyChange();
}
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
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 };
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, {
enumerable: true,
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
self.rev++;
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
value = newVal;
self.observe(newVal, obj);
obj.__owl__.rev!++;
let parent = obj;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== obj);
self.notifyChange();
}
}
});
this.observe(value, obj);
}
}
@@ -1,4 +1,6 @@
import { Context, QWeb, UTILS } from "./qweb_core";
import { Context } from "./context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
/**
* Owl QWeb Directives
@@ -14,11 +16,10 @@ import { Context, QWeb, UTILS } from "./qweb_core";
* - t-log
*/
//------------------------------------------------------------------------------
// 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;
@@ -30,42 +31,48 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
return;
}
if (typeof value === "string") {
let exprID = value;
if (!(value in ctx.definedVariables)) {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (!ctx.parentNode) {
throw new Error("Should not have a text node without a parent");
}
if (ctx.escaping) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let fragID = ctx.generateID();
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
return;
}
if (value instanceof NodeList) {
for (let node of Array.from(value)) {
if (value.xml instanceof NodeList) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
}
} else {
let fragID = ctx.generateID();
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
}
QWeb.addDirective({
@@ -109,15 +116,19 @@ QWeb.addDirective({
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.definedVariables[varName] = formattedValue;
ctx.variables[variable] = varName;
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
}
} else {
ctx.variables[variable] = node.childNodes;
ctx.variables[variable] = {
xml: node.childNodes
};
}
return true;
}
@@ -189,30 +200,25 @@ QWeb.addDirective({
tempCtx.nextID = ctx.rootContext.nextID;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
var definedVariables = Object.assign(
{},
ctx.definedVariables,
tempCtx.definedVariables
);
ctx.rootContext.nextID = tempCtx.nextID;
// open new scope, if necessary
const hasNewVariables = Object.keys(definedVariables).length > 0;
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
if (hasNewVariables) {
ctx.addLine("{");
ctx.indent();
}
// add new variables, if any
for (let key in definedVariables) {
ctx.addLine(`let ${key} = ${definedVariables[key]}`);
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
}
// todo: handle XML variables...
}
}
// compile sub template
const subCtx = ctx
.subContext("caller", nodeCopy)
.subContext("variables", Object.create(vars))
.subContext("definedVariables", Object.create(definedVariables));
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
@@ -222,6 +228,10 @@ QWeb.addDirective({
ctx.addLine("}");
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
@@ -240,34 +250,26 @@ QWeb.addDirective({
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
);
ctx.addLine(
`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`
);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
ctx.addLine(
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
);
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let valuesID = ctx.generateID();
ctx.addLine(
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
);
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`);
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
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");
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (
@@ -299,7 +301,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
priority: 99,
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
@@ -310,7 +312,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
priority: 99,
priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
+167
View File
@@ -0,0 +1,167 @@
import { compileExpr, QWebVar } from "./expression_parser";
//------------------------------------------------------------------------------
// 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;
shouldDefineResult: boolean = false;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
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.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;
}
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(/\{\{.*?\}\}/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 + "`";
}
}
+264
View File
@@ -0,0 +1,264 @@
/**
* Owl QWeb Expression Parser
*
* Owl needs in various contexts to be able to understand the structure of a
* string representing a javascript expression. The usual goal is to be able
* to rewrite some variables. For example, if a template has
*
* ```xml
* <t t-if="computeSomething({val: state.val})">...</t>
* ```
*
* this needs to be translated in something like this:
*
* ```js
* if (context["computeSomething"]({val: context["state"].val})) { ... }
* ```
*
* This file contains the implementation of an extremely naive tokenizer/parser
* and evaluator for javascript expressions. The supported grammar is basically
* only expressive enough to understand the shape of objects, of arrays, and
* various operators.
*/
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
type TKind =
| "LEFT_BRACE"
| "RIGHT_BRACE"
| "LEFT_BRACKET"
| "RIGHT_BRACKET"
| "LEFT_PAREN"
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "SYMBOL"
| "OPERATOR"
| "COLON";
interface Token {
type: TKind;
value: string;
size?: number;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
"]": "RIGHT_BRACKET",
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
return false;
}
let i = 1;
let cur;
while (expr[i] && expr[i] !== start) {
cur = expr[i];
s += cur;
if (cur === "\\") {
i++;
cur = expr[i];
if (!cur) {
throw new Error("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new Error("Invalid expression");
}
s += start;
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
let i = 1;
while (expr[i] && expr[i].match(/[0-9]|\./)) {
s += expr[i];
i++;
}
return { type: "VALUE", value: s };
} else {
return false;
}
};
let tokenizeSymbol: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1;
while (expr[i] && expr[i].match(/\w/)) {
s += expr[i];
i++;
}
if (s in WORD_REPLACEMENT) {
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
}
return { type: "SYMBOL", value: s };
} else {
return false;
}
};
const tokenizeStatic: Tokenizer = function(expr) {
const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
}
return false;
};
const tokenizeOperator: Tokenizer = function(expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
}
}
return false;
};
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
* example: `tokenize("1 + b")` will return:
* ```js
* [
* {type: "VALUE", value: "1"},
* {type: "OPERATOR", value: "+"},
* {type: "SYMBOL", value: "b"}
* ]
* ```
*/
export function tokenize(expr: string): Token[] {
const result: Token[] = [];
let token: boolean | Token = true;
while (token) {
expr = expr.trim();
if (expr) {
for (let tokenizer of TOKENIZERS) {
token = tokenizer(expr);
if (token) {
result.push(token);
expr = expr.slice(token.size || token.value.length);
break;
}
}
} else {
token = false;
}
}
if (expr.length) {
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
}
return result;
}
//------------------------------------------------------------------------------
// Expression "evaluator"
//------------------------------------------------------------------------------
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
* proper lookups in the context.
*
* Usually, this kind of code would be very simple to do if we had an AST (so,
* if we had a javascript parser), since then, we would only need to find the
* variables and replace them. However, a parser is more complicated, and there
* are no standard builtin parser API.
*
* Since this method is applied to simple javasript expressions, and the work to
* be done is actually quite simple, we actually can get away with not using a
* parser, which helps with the code size.
*
* Here is the heuristic used by this method to determine if a token is a
* variable:
* - by default, all symbols are considered a variable
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
} else {
token.value = `context['${token.value}']`;
}
}
}
result += token.value;
}
return result;
}
+284
View File
@@ -0,0 +1,284 @@
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 }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.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-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.rootContext.shouldDefineOwner = true;
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
}, 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 }) {
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}];`);
}
});
+4
View File
@@ -0,0 +1,4 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
+232 -315
View File
@@ -1,4 +1,6 @@
import { VNode, h } from "./vdom";
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context";
/**
* Owl QWeb Engine
@@ -56,40 +58,12 @@ export interface Directive {
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
const DISABLED_TAGS = [
"input",
"textarea",
"button",
"select",
"option",
"optgroup"
];
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
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",
@@ -97,21 +71,34 @@ const NODE_HOOKS_PARAMS = {
};
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
export const UTILS: Utils = {
h: h,
objectToAttrString(obj: Object): string {
let classes: string[] = [];
for (let k in obj) {
if (obj[k]) {
classes.push(k);
const UTILS: Utils = {
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return classes.join(" ");
return true;
}
};
@@ -119,7 +106,27 @@ function parseXML(xml: string): Document {
const parser = new DOMParser();
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;
}
@@ -129,11 +136,20 @@ let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb {
export class QWeb extends EventBus {
templates: { [name: string]: Template } = {};
utils = UTILS;
static widgets = Object.create(null);
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[] = [];
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
@@ -142,34 +158,47 @@ export class QWeb {
// 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;
isUpdating: boolean = false;
constructor(data?: string) {
super();
if (data) {
this.addTemplates(data);
}
this.addTemplate("default", "<div></div>");
}
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) {
if (QWeb.widgets[name]) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.widgets[name] = Component;
QWeb.components[name] = Component;
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string) {
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
@@ -178,12 +207,12 @@ export class QWeb {
}
/**
* 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;
@@ -232,9 +261,7 @@ export class QWeb {
return a + b;
}) > 1
) {
throw new Error(
"Only one conditional branching directive is allowed per node"
);
throw new Error("Only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
let textNode;
@@ -264,50 +291,93 @@ export class QWeb {
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element): 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 = {}): string {
const vnode = this.render(name, context);
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 (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
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.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.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 (!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(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace(
/`/g,
"'"
)}': ${e.message}`
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
console.log(
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
template.toString()
);
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
@@ -328,17 +398,25 @@ export class QWeb {
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
@@ -354,29 +432,30 @@ export class QWeb {
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split("-")[0];
if (!(dName in DIRECTIVE_NAMES)) {
let dName = attrName.slice(2).split(/-|\./)[0];
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++) {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-")
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on") {
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
@@ -445,11 +524,7 @@ export class QWeb {
}
}
_compileGenericNode(
node: ChildNode,
ctx: Context,
withHandlers: boolean = true
): number {
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
@@ -473,66 +548,76 @@ export class QWeb {
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if (
(key === "readonly" && node.nodeName === "input") ||
node.nodeName === "textarea"
) {
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
@@ -573,6 +658,9 @@ export class QWeb {
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
@@ -589,9 +677,7 @@ export class QWeb {
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
@@ -607,172 +693,3 @@ export class QWeb {
}
}
}
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: any } = {};
definedVariables: { [key: string]: string } = {};
escaping: boolean = false;
parentNode: 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;
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 === this.rootContext && this.parentNode) {
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;
}
formatExpression(e: string): string {
e = e.trim();
if (e[0] === "{" && e[e.length - 1] === "}") {
const innerExpr = e
.slice(1, -1)
.split(",")
.map(p => {
let [key, val] = p.trim().split(":");
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this.formatExpression(val)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (let i = 0, ilen = chars.length; i < ilen; i++) {
let c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
if (!(invar in this.definedVariables)) {
invar =
WORD_REPLACEMENT[invar] ||
(invar in this.variables &&
this.formatExpression(this.variables[invar])) ||
"context['" + invar + "']";
}
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
return result;
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x}}' -> x
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -2));
}
matches = s.match(/\#\{.*?\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -1));
}
let formatter = expr => "${" + this.formatExpression(expr) + "}";
let r = s
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
return "`" + r + "`";
}
}
-556
View File
@@ -1,556 +0,0 @@
import { QWeb, UTILS } from "./qweb_core";
/**
* 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-widget/t-props/t-keepalive
* - t-mounted
*/
//------------------------------------------------------------------------------
// 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(elm: HTMLElement, name: string) {
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(
elm: HTMLElement,
name: string,
rm: () => void
) {
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.elm, '${name}');`,
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-widget
//------------------------------------------------------------------------------
/**
* The t-widget directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-widget directive for the following
* situation:
* ```xml
* <t t-widget="child"
* t-key="'somestring'"
* t-props="{flag:state.flag}"
* t-transition="fade"/>
* ```
*
* ```js
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-widget directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // def3 is the deferred that will contain later either the new widget
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the widget was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a widget, currently rendering, but not ready yet, and which was
* // rendered with different props, we do not want to wait for it to be ready,
* // then update it. We simply destroy it, and start anew.
* if (
* w4 &&
* w4.__owl__.renderPromise &&
* !w4.__owl__.isStarted &&
* props4 !== w4.__owl__.renderProps
* ) {
* w4.destroy();
* w4 = false;
* }
*
* if (!w4) {
* // in this situation, we need to create a new widget. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.widgets["child"];
* if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'");
* }
* w4 = new W4(owner, props4);
*
* let utils = this.utils;
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the widget from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // _prepare is called, to basically call willStart, then render the
* // widget
* def3 = w4._prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the widget). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // widget at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the _mount method will patch the widget vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the widget
* utils.transitionInsert(vn.elm, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // widget, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn.elm, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // widget, so we can reuse it later whenever we update the widget
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update
* def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the widget's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "widget",
extraNames: ["props", "keepalive"],
priority: 100,
atNodeEncounter({ ctx, value, node }): boolean {
ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let props = node.getAttribute("t-props");
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
// t-on- events and t-transition
const events: [string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent!]);
} else if (name === "t-transition") {
transition = attributes[i].textContent!;
}
}
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
if (props) {
props = ctx.formatExpression(props);
}
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let widgetID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`);
}
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
ctx.addLine(`c${ctx.parentNode}.push(null);`);
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? `String(-${widgetID} - i)`
: String(widgetID);
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
}
let finalizeWidgetCode = `w${widgetID}.${
keepAlive ? "unmount" : "destroy"
}();`;
if (ref) {
finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
}
if (transition) {
finalizeWidgetCode = `let finalize = () => {
${finalizeWidgetCode}
};
utils.transitionRemove(vn.elm, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let updateClassCode = "";
if (classAttr || tattClass || styleAttr || tattStyle) {
let classCode = "";
if (classAttr) {
classCode =
classAttr
.split(" ")
.map(c => `vn.elm.classList.add('${c}')`)
.join(";") + ";";
}
if (tattClass) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
classCode = `for (let k in ${attVar}) {
if (${attVar}[k]) {
vn.elm.classList.add(k);
}
}`;
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
}
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
}
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`
);
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false`);
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
// new widget
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
ctx.addLine(
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
for (let [event, method] of events) {
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update widget
ctx.addLine(
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
);
ctx.closeIf();
ctx.addLine(`extra.promises.push(def${defID});`);
if (
node.hasAttribute("t-if") ||
node.hasAttribute("t-else") ||
node.hasAttribute("t-elif")
) {
ctx.closeIf();
}
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}](); }`
);
}
});
+48
View File
@@ -0,0 +1,48 @@
import { Component } from "../component/component";
import { Destination, RouterEnv } from "./Router";
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
export const LINK_TEMPLATE = `
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>`;
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
template = LINK_TEMPLATE_NAME;
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);
}
}
+262
View File
@@ -0,0 +1,262 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { makeDirective } from "./directive";
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
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.register("__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);
}
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
window.addEventListener("popstate", (this as any)._listener);
// setup link and directive
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
QWeb.addDirective(makeDirective(<RouterEnv>env));
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
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);
const initialName = this.currentRouteName;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
this.setUrlFromPath(finalPath);
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
if (this.currentRouteName !== initialName) {
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 url = location.origin + path;
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];
}
}
return 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 {};
}
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;
}
+28
View File
@@ -0,0 +1,28 @@
import { RouterEnv } from "./Router";
export function makeDirective(env: RouterEnv) {
return {
name: "routecomponent",
priority: 13,
atNodeEncounter({ node }): boolean {
let first = true;
const router = env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
// make new t t-component element
const comp = node.ownerDocument.createElement("t");
comp.setAttribute("t-component", "__component__" + route.name);
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
first = false;
for (let param of route.params) {
comp.setAttribute(param, `env.router.currentParams.${param}`);
}
node.appendChild(comp);
}
}
node.removeAttribute("t-routecomponent");
return false;
}
};
}
-283
View File
@@ -1,283 +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, set, 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;
set: any;
getters: { [name: string]: (payload?) => any };
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.trigger.bind(this, "update");
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
if (this.debug) {
this.history.push({ state: this.state });
}
this.set = this.observer.set.bind(this.observer);
for (let entry of Object.entries(config.getters || {})) {
const name: string = entry[0];
const func: (...any) => any = entry[1];
this.getters[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,
set: this.set,
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;
}
}
//------------------------------------------------------------------------------
// 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;
}
let nextID = 1;
export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null;
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;
};
}
return function<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>
) {
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {});
const storeProps = mapStateToProps(
env.store.state,
ownProps,
env.store.getters
);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).storeHash = hashFunction(
{
state: env.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() {
this.env.store.on("update", this, this._checkUpdate);
super._callMounted();
}
willUnmount() {
this.env.store.off("update", this);
super.willUnmount();
}
_checkUpdate() {
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStateToProps(
this.env.store.state,
ownProps,
this.env.store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = hashFunction(
{
state: this.env.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;
this._updateProps(ownProps, false);
}
}
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
if ((<any>this.__owl__).ownProps !== nextProps) {
(<any>this.__owl__).currentStoreProps = mapStateToProps(
this.env.store.state,
nextProps,
this.env.store.getters
);
}
(<any>this.__owl__).ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
(<any>this.__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 = `ConnectedComponent${nextID++}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
};
}
+119
View File
@@ -0,0 +1,119 @@
import { Component, Env } from "../component/component";
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
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(
scope?: Object,
vars?: any
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
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).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(scope, vars);
}
/**
* 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 __updateProps(nextProps: P, f, p, s, v) {
this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, p, s, v);
}
__updateStoreProps(nextProps): boolean {
const store = (this.__owl__ as any).store;
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 !== (this.__owl__ as any).storeHash) {
(this.__owl__ as any).storeHash = storeHash;
didChange = true;
}
(this.__owl__ as any).rev = store.observer.rev;
return didChange;
}
async __checkUpdate() {
const observer = (this.__owl__ as any).store.observer;
if (observer.rev === (this.__owl__ as any).rev) {
// update was already done by updateProps, from parent
return;
}
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
}
}
}
+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);
}
}
}
}
+9 -11
View File
@@ -10,12 +10,14 @@
* - debounce
*/
export function whenReady(fn) {
if (document.readyState === "complete") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn, false);
}
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
if (document.readyState !== "loading") {
resolve();
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function() {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -74,11 +76,7 @@ export function escape(str: string | number | undefined): string {
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
export function debounce(
func: Function,
wait: number,
immediate?: boolean
): Function {
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout;
return function(this: any) {
const context = this;
+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;
+39 -363
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;
@@ -59,14 +74,7 @@ function vnode(
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return {
sel: sel,
data: data,
children: children,
text: text,
elm: elm,
key: key
};
return { sel, data, children, text, elm, key };
}
//------------------------------------------------------------------------------
@@ -116,14 +124,7 @@ function createKeyToOldIdx(
return map;
}
const hooks: (keyof Module)[] = [
"create",
"update",
"remove",
"destroy",
"pre",
"post"
];
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number,
@@ -145,13 +146,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
function createRmCb(childElm: Node, listeners: number) {
@@ -186,19 +181,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, tag)
: api.createElement(tag));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i];
@@ -244,8 +234,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
cbs.destroy[i](vnode);
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j];
@@ -274,13 +263,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
cbs.remove[i](ch, rm);
if (
isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
isDef((i = i.remove))
) {
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
i(ch, rm);
} else {
rm();
@@ -342,11 +326,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
@@ -373,11 +353,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
@@ -386,31 +362,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, iLen: number, hook: any;
if (
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm as Node);
@@ -418,20 +379,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
cbs.update[i](oldVnode, vnode);
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(
elm,
oldCh as Array<VNode>,
ch as Array<VNode>,
insertedVnodeQueue
);
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(
@@ -443,23 +398,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
insertedVnodeQueue
);
} else if (isDef(oldCh)) {
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
}
api.setTextContent(elm, vnode.text as string);
}
@@ -517,21 +462,13 @@ interface DOMAPI {
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
insertBefore: (
parentNode: Node,
newNode: Node,
referenceNode: Node | null
) => void;
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
removeChild: (node: Node, child: Node) => void;
appendChild: (node: Node, child: Node) => void;
parentNode: (node: Node) => Node;
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 {
@@ -550,11 +487,7 @@ function createComment(text: string): Comment {
return document.createComment(text);
}
function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node | null
): void {
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode);
}
@@ -582,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,
@@ -610,10 +527,6 @@ const htmlDomApi = {
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment
} as DOMAPI;
//------------------------------------------------------------------------------
@@ -651,21 +564,13 @@ 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 {
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;
if (childData !== undefined) {
addNS(
childData,
(children[i] as VNode).children as VNodes,
children[i].sel
);
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
@@ -704,13 +609,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
if (children !== undefined) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined
);
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
@@ -723,226 +622,3 @@ export function h(sel: any, b?: any, c?: any): VNode {
}
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;
export const patch = init([eventListenersModule, attrsModule, propsModule]);
+54 -42
View File
@@ -1,37 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with t-widget 1`] = `
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);
//WIDGET
//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 def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__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.destroy();
};
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -39,39 +44,44 @@ exports[`animations t-transition combined with t-widget 1`] = `
}"
`;
exports[`animations t-transition combined with t-widget and t-if 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);
if (context['state'].display) {
//WIDGET
//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 def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__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.destroy();
};
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -83,15 +93,16 @@ exports[`animations t-transition combined with t-widget 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);
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn.elm, 'jupiler');
utils.transitionInsert(vn, 'jupiler');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'jupiler', rm);
utils.transitionRemove(vn, 'jupiler', rm);
},
};
c1.push({text: \`blue\`});
@@ -102,15 +113,16 @@ 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);
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn.elm, 'chimay');
utils.transitionInsert(vn, 'chimay');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'chimay', rm);
utils.transitionRemove(vn, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
-276
View File
@@ -1,276 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};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 {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = {a: context['state'].a,b: context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){for (let k in _5) {
if (_5[k]) {
vn.elm.classList.add(k);
}
}}};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 {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`composition sub widgets with some state rendered in a loop 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
context = Object.create(context);
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
var _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _length3; i++) {
context.number_first = i === 0;
context.number_last = i === _length3 - 1;
context.number_parity = i % 2 === 0 ? 'even' : 'odd';
context.number_index = i;
context.number = _3[i];
context.number_value = _4[i];
//WIDGET
let key8 = context['number'];
let _5_index = c1.length;
c1.push(null);
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode && props7 !== w7.__owl__.renderProps) {
w7.destroy();
w7 = false
}
if (!w7) {
let widgetKey7 = \`ChildWidget\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
}
return vn1;
}"
`;
exports[`composition t-widget with dynamic value 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = context['state'].widget;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`composition t-widget with dynamic value 2 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`Widget\${context['state'].widget}\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = 'somestring';
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let props4 = {flag: context['state'].flag};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, 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 {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
-33
View File
@@ -1,33 +0,0 @@
function anonymous(context, extra) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let W4 = context.widgets['Child'];
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
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;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
w4.destroy();
};
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
}
+128 -14
View File
@@ -1,5 +1,5 @@
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 {
makeDeferred,
makeTestFixture,
@@ -18,7 +18,7 @@ import {
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - qweb: a new QWeb instance
// - env: a WEnv, necessary to create new widgets
// - env: a WEnv, necessary to create new components
// - cssEl: a stylesheet injected into the dom
let fixture: HTMLElement;
@@ -170,16 +170,13 @@ describe("animations", () => {
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-widget", async () => {
test("t-transition combined with component", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
components = { Child: Child };
}
class Child extends Widget {}
const widget = new Parent(env);
@@ -209,16 +206,16 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
});
test("t-transition combined with t-widget and t-if", async () => {
test("t-transition combined with t-component and t-if", async () => {
expect.assertions(8);
env.qweb.addTemplate(
"Parent",
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
components = { Child: Child };
state = { display: true };
}
class Child extends Widget {}
@@ -253,11 +250,11 @@ describe("animations", () => {
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active">blue</span></div>'
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
);
def.resolve();
});
@@ -287,4 +284,121 @@ describe("animations", () => {
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<span t-if="state.flag" t-transition="chimay">blue</span>
</div>
</templates>`
);
class Parent extends Widget {
constructor(parent) {
super(parent);
this.state = { flag: false };
}
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent(env);
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
let def = makeDeferred();
let phase = "enter";
patchNextFrame(cb => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
// click display the span
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
button!.click();
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>
<span t-name="Child">blue</span>
</templates>`
);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
constructor(parent) {
super(parent);
this.state = { flag: false };
}
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent(env);
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
let def = makeDeferred();
let phase = "enter";
patchNextFrame(cb => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
// click display the span
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
button!.click();
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
// 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);
//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__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
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();
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;},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.forceUpdate, extra.patchQueue);
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
@@ -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' (widget 'TestWidget')");
}
});
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
@@ -65,7 +52,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'message' (widget 'TestWidget')");
}).toThrow("Missing props 'message' (component 'TestWidget')");
});
test("validate simple types", async () => {
@@ -93,7 +80,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
}
});
@@ -122,11 +109,10 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
}
});
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
static props = { p: [String, Boolean] };
@@ -139,7 +125,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
});
test("can validate an optional props", async () => {
@@ -149,16 +135,14 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: "hey" });
new TestWidget(env, { });
new TestWidget(env, {});
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow();
});
test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget {
static props = { p: { type: Array, element: String } };
@@ -222,7 +206,7 @@ describe("props validation", () => {
type: Object,
shape: {
id: Number,
url: [Boolean, {type: Array, element: Number}],
url: [Boolean, { type: Array, element: Number }]
}
}
};
@@ -230,21 +214,109 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12]} });
new TestWidget(env, { p: { id: 1, url: [12] } });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true]} });
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 {
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", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
static defaultProps = { p: 4 };
}
const w = new TestWidget(env, {});
@@ -253,11 +325,19 @@ describe("default props", () => {
test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
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({});
const w = new TestWidget(env, { p: 1 });
await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
await w.__updateProps({});
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", () => {
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
test("throw error if callback is undefined", () => {
expect.assertions(1);
const bus = new EventBus();
expect(() => bus.on("event", {}, <any>undefined)).toThrow(
`Missing callback`
);
expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
});
test("can unsubscribe", () => {
+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")');
});
});
+126
View File
@@ -0,0 +1,126 @@
/**
* Doc Link Checker
*
* We define here a test to make sure that there are no dead link in the Owl
* documentation.
*/
import * as fs from "fs";
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
// files to be checked
function getFiles(): string[] {
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
const MAINREADME = "README.md";
return DOCFILES.concat(MAINREADME);
}
test("All markdown links work", () => {
let linkNumber = 0;
let invalidLinkNumber = 0;
const files = getFiles();
const data = readDocData(files);
for (let file of data) {
for (let link of file.links) {
// DEBUG: uncomment next line
// console.warn(`Checking "${link.name}" in "${file.name}"`);
linkNumber++;
if (!isLinkValid(link, file, data)) {
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
invalidLinkNumber++;
}
}
}
expect(invalidLinkNumber).toBe(0);
expect(linkNumber).toBeGreaterThan(10);
});
interface MarkDownLink {
name: string;
link: string;
}
interface MarkDownSection {
name: string;
slug: string;
}
interface FileData {
name: string;
links: MarkDownLink[];
sections: MarkDownSection[];
}
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
const parts = link.link.split("#");
const currentParts = current.name.split("/");
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
const fullName = path + parts[0];
if (parts.length === 1) {
// no # in url
if (parts[0].endsWith(".md")) {
// it is a local md file
if (!files.find(f => f.name === fullName)) {
return false;
}
}
} else {
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
if (!file) {
return false;
}
if (!file.sections.find(s => s.slug === parts[1])) {
return false;
}
}
return true;
}
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
function slugify(str) {
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----";
const p = new RegExp(a.split("").join("|"), "g");
return str
.toString()
.toLowerCase()
.replace(/\//g, "") // remove /
.replace(/\s+/g, "-") // Replace spaces with -
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, "-and-") // Replace & with and
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
.replace(/\-\-+/g, "-") // Replace multiple - with single -
.replace(/^-+/, "") // Trim - from start of text
.replace(/-+$/, ""); // Trim - from end of text
}
function readDocData(files: string[]): FileData[] {
const result: FileData[] = [];
for (let file of files) {
const fileData: FileData = {
name: file,
links: [],
sections: []
};
const content = fs.readFileSync(file, { encoding: "utf8" });
let m;
// get links info
do {
m = LINK_REGEXP.exec(content);
if (m) {
fileData.links.push({ name: m[0], link: m[2] });
}
} while (m);
// get sections info
do {
m = HEADING_REGEXP.exec(content);
if (m) {
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
}
} while (m);
result.push(fileData);
}
return result;
}
+30 -13
View File
@@ -1,8 +1,9 @@
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";
export function nextMicroTick(): Promise<void> {
return Promise.resolve();
@@ -68,26 +69,42 @@ export function renderToDOM(
return result.elm as HTMLElement;
}
export function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {}
): string {
/**
* 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 = {}): string {
const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML;
const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context)) {
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) {
input.value = value;
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change"));
return nextTick();
}
-323
View File
@@ -1,323 +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("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("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
+78 -133
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
@@ -24,6 +24,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("hello vdom");
});
test("simple dynamic value", () => {
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
});
test("empty div", () => {
qweb.addTemplate("test", "<div></div>");
expect(renderToString(qweb, "test")).toBe("<div></div>");
@@ -42,9 +52,7 @@ describe("static templates", () => {
describe("error handling", () => {
test("invalid xml", () => {
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
"Invalid XML in template"
);
expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
});
test("template with text node and tag", () => {
@@ -61,6 +69,7 @@ describe("error handling", () => {
test("cannot add twice the same template", () => {
qweb.addTemplate("test", `<t></t>`);
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
});
@@ -79,29 +88,14 @@ describe("error handling", () => {
test("nice error when t-on is evaluated with a missing event", () => {
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
expect(() =>
qweb.render("templatename", { someMethod() {} }, { handlers: [] })
).toThrow("Missing event name with t-on directive");
});
test("error when compiled code is invalid", () => {
qweb.addTemplate(
"templatename",
`<div t-att-hey="}/^function invalid{{>'"></div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Invalid generated code while compiling template 'templatename': Unexpected token }"
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
"Missing event name with t-on directive"
);
});
test("error when unknown directive", () => {
qweb.addTemplate(
"templatename",
`<div t-best-beer="rochefort 10">test</div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
});
});
@@ -118,9 +112,7 @@ describe("t-esc", () => {
test.skip("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe(
"<span>&lt;ok&gt;</span>"
);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
});
test("escaping on a node", () => {
@@ -152,9 +144,7 @@ describe("t-raw", () => {
test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe(
"<div><ok></ok></div>"
);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
});
test("t-raw and another sibling node", () => {
@@ -167,39 +157,28 @@ describe("t-raw", () => {
describe("t-set", () => {
test("set from attribute literal", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
});
test("t-set and t-if", () => {
qweb.addTemplate(
"test",
`<div >
`<div>
<t t-set="v" t-value="value"/>
<t t-if="v === 'ok'">grimbergen</t>
</div>`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
"<div>grimbergen</div>"
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
});
test("set from body literal", () => {
qweb.addTemplate(
"test",
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
);
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
expect(renderToString(qweb, "test")).toBe("ok");
});
test("set from attribute lookup", () => {
qweb.addTemplate(
"test",
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
});
@@ -231,18 +210,12 @@ describe("t-set", () => {
});
test("value priority", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
});
test("evaluate value expression", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
});
@@ -267,25 +240,19 @@ describe("t-set", () => {
"test",
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
"<div>45</div>"
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
});
});
describe("t-if", () => {
test("boolean value true condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: true })).toBe(
"<div>ok</div>"
);
expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
});
test("boolean value false condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: false })).toBe(
"<div></div>"
);
expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
});
test("boolean value condition missing", () => {
@@ -302,9 +269,7 @@ describe("t-if", () => {
<t t-else="">beer</t></div>
`
);
expect(renderToString(qweb, "test", { color: "red" })).toBe(
"<div>red is dead</div>"
);
expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
});
test("boolean value condition else", () => {
@@ -330,9 +295,7 @@ describe("t-if", () => {
`
);
const result = trim(renderToString(qweb, "test", { condition: false }));
expect(result).toBe(
"<div><span>begin</span>fail-else<span>end</span></div>"
);
expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
});
test("can use some boolean operators in expressions", () => {
@@ -357,9 +320,7 @@ describe("t-if", () => {
m: 5,
n: 2
};
expect(normalize(renderToString(qweb, "test", context))).toBe(
"<div>andormgtnlt</div>"
);
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
});
});
@@ -478,16 +439,13 @@ describe("attributes", () => {
});
test("format expression, other format", () => {
qweb.addTemplate("test", `<div t-attf-foo="#{value + 37}"/>`);
qweb.addTemplate("test", `<div t-attf-foo="{{value + 37}}"/>`);
const result = renderToString(qweb, "test", { value: 5 });
expect(result).toBe(`<div foo="42"></div>`);
});
test("format multiple", () => {
qweb.addTemplate(
"test",
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
);
qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
const result = renderToString(qweb, "test", {
value1: 0,
value2: 1,
@@ -522,11 +480,14 @@ describe("attributes", () => {
expect(result).toBe(`<div class="hello world"></div>`);
});
test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`);
});
test("t-att-class with object", () => {
qweb.addTemplate(
"test",
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
);
qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
expect(result).toBe(`<div class="static a e"></div>`);
});
@@ -540,6 +501,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"/>');
@@ -555,10 +523,7 @@ describe("t-call (template calling", () => {
test("with unused setbody", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate(
"caller",
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
);
qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected);
});
@@ -644,25 +609,13 @@ describe("foreach", () => {
"test",
`
<div>
<t t-foreach="5" t-as="elem">
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
<t t-foreach="Array(5)" t-as="elem">
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_index"/>)
</t>
</div>`
);
const result = trim(renderToString(qweb, "test"));
const expected = `<div>-first(even)-(odd)-(even)-(odd)-last(even)</div>`;
expect(result).toBe(expected);
});
test("iterate, integer param", () => {
qweb.addTemplate(
"test",
`<div><t t-foreach="3" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t></div>`
);
const result = trim(renderToString(qweb, "test"));
const expected = `<div>[0:00][1:11][2:22]</div>`;
const expected = `<div>-first(0)-(1)-(2)-(3)-last(4)</div>`;
expect(result).toBe(expected);
});
@@ -672,14 +625,12 @@ describe("foreach", () => {
`
<div>
<t t-foreach="value" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>
</div>`
);
const result = trim(
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
);
const expected = `<div>[0:a1-even][1:b2-odd][2:c3-even]</div>`;
const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
expect(result).toBe(expected);
});
@@ -725,14 +676,8 @@ describe("foreach", () => {
describe("misc", () => {
test("global", () => {
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate(
"_callee-uses-foo",
`<span t-esc="foo">foo default</span>`
);
qweb.addTemplate(
"_callee-asc-toto",
`<div t-raw="toto">toto default</div>`
);
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
qweb.addTemplate(
"caller",
`
@@ -845,10 +790,7 @@ describe("t-on", () => {
});
test("can bind handlers with object arguments", () => {
qweb.addTemplate(
"test",
`<button t-on-click="add({val: 5})">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
let a = 1;
const node = renderToDOM(
qweb,
@@ -866,10 +808,7 @@ describe("t-on", () => {
test("can bind handlers with empty object", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<button t-on-click="doSomething({})">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
const node = renderToDOM(
qweb,
"test",
@@ -883,12 +822,9 @@ describe("t-on", () => {
(<HTMLElement>node).click();
});
test("can bind handlers with empty object (with non empty inner string", () => {
test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<button t-on-click="doSomething({ })">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
const node = renderToDOM(
qweb,
"test",
@@ -1108,7 +1044,7 @@ describe("loading templates", () => {
test("does not crash if string does not have templates", () => {
const data = "";
qweb.addTemplates(data);
expect(Object.keys(qweb.templates)).toEqual(["default"]);
expect(Object.keys(qweb.templates)).toEqual([]);
});
});
@@ -1188,13 +1124,10 @@ describe("whitespace handling", () => {
describe("t-key", () => {
test("can use t-key directive on a node", () => {
qweb.addTemplate(
"test",
`<div t-key="beer.id"><t t-esc="beer.name"/></div>`
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
"<div>Chimay Rouge</div>"
);
expect(
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
).toBe("<div>Chimay Rouge</div>");
});
test("t-key directive in a list", () => {
@@ -1245,3 +1178,15 @@ 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);
});
});
+158
View File
@@ -0,0 +1,158 @@
import { compileExpr, tokenize } from "../../src/qweb/expression_parser";
describe("tokenizer", () => {
test("simple tokens", () => {
expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("{ }}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
expect(tokenize("abcde")).toEqual([{ type: "SYMBOL", value: "abcde" }]);
expect(tokenize("_ab2")).toEqual([{ type: "SYMBOL", value: "_ab2" }]);
expect(tokenize("$ab2")).toEqual([{ type: "SYMBOL", value: "$ab2" }]);
expect(tokenize("ABC")).toEqual([{ type: "SYMBOL", value: "ABC" }]);
expect(tokenize("{a: 2}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "SYMBOL", value: "a" },
{ type: "COLON", value: ":" },
{ type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" }
]);
});
test("various operators", () => {
expect(tokenize(">= <= < > !== !=")).toEqual([
{ type: "OPERATOR", value: ">=" },
{ type: "OPERATOR", value: "<=" },
{ type: "OPERATOR", value: "<" },
{ type: "OPERATOR", value: ">" },
{ type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" }
]);
});
test("strings", () => {
expect(() => tokenize("'")).toThrow("Invalid expression");
expect(() => tokenize("'\\")).toThrow("Invalid expression");
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
expect(() => tokenize('"')).toThrow("Invalid expression");
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
});
});
describe("expression evaluation", () => {
test("simple static values", () => {
expect(compileExpr("1", {})).toBe("1");
expect(compileExpr("1 ", {})).toBe("1");
expect(compileExpr("'some string#/, {' ", {})).toBe("'some string#/, {'");
expect(compileExpr("{ } ", {})).toBe("{}");
expect(compileExpr("{a: 1} ", {})).toBe("{a:1}");
expect(compileExpr("{a: 1, b: 2 } ", {})).toBe("{a:1,b:2}");
expect(compileExpr("[] ", {})).toBe("[]");
expect(compileExpr("[1] ", {})).toBe("[1]");
expect(compileExpr("['1', '2'] ", {})).toBe("['1','2']");
expect(compileExpr("['1', \"2\"] ", {})).toBe("['1',\"2\"]");
});
test("various types of 'words'", () => {
expect(compileExpr("true", {})).toBe("true");
expect(compileExpr("false", {})).toBe("false");
expect(compileExpr("debugger", {})).toBe("debugger");
});
test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
});
test("objects and sub objects", () => {
expect(compileExpr("{a:{b:1}} ", {})).toBe("{a:{b:1}}");
});
test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
});
test("arrays and objects", () => {
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
});
test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
});
test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
});
test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
});
test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"context['a']?{test:context['c']}:[1,context['u']]"
);
});
test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
});
test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
);
});
});
+84
View File
@@ -0,0 +1,84 @@
import { Component } from "../../src/component/component";
import { Link, LINK_TEMPLATE_NAME } 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, any> {
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_NAME].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, any> {
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");
});
});
+17
View File
@@ -0,0 +1,17 @@
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);
delete QWeb.DIRECTIVE_NAMES.routecomponent;
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
// remove component defined inroutes
for (let key in QWeb.components) {
if (key.startsWith("__component__")) {
delete QWeb.components[key];
}
}
}
}
@@ -0,0 +1,24 @@
// 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);
if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`)
}
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) {
slot4(context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
}
return vn3;
}"
`;
@@ -0,0 +1,159 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`router directive t-routecomponent can render parameterized route 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);
if (context['env'].router.currentRouteName==='book') {
//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 = {title:context['env'].router.currentParams.title};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-routecomponent can render parameterized route with suffixes 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);
if (context['env'].router.currentRouteName==='book') {
//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 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-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;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='about') {
//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 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__about\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
else if (context['env'].router.currentRouteName==='users') {
//COMPONENT
let def6;
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let componentKey7 = \`__component__users\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
}
return vn1;
}"
`;
+106
View File
@@ -0,0 +1,106 @@
import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
describe("router directive t-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">
<t t-routecomponent="1" />
</div>
<span t-name="About">About</span>
<span t-name="Users">Users</span>
</templates>
`);
class About extends Component<any, any, any> {}
class Users extends Component<any, any, any> {}
class App extends Component<any, any, any> {
components = { About, Users };
}
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.App.fn.toString()).toMatchSnapshot();
});
test("can render parameterized route", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-routecomponent="1" />
</div>
<span t-name="Book">Book <t t-esc="props.title"/></span>
</templates>
`);
class Book extends Component<any, any, any> {}
class App extends Component<any, any, any> {
components = { Book };
}
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>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("can render parameterized route with suffixes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-routecomponent="1" />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates>
`);
class Book extends Component<any, any, any> {
get incVal() {
return this.props.val + 1;
}
}
class App extends Component<any, any, any> {
components = { Book };
}
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>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
});
+180
View File
@@ -0,0 +1,180 @@
import { Destination, Router, RouterEnv, Route } from "../../src/router/Router";
import { makeTestEnv } 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"}`);
});
});
describe("routeToPath", () => {
const routeToPath = Router.prototype["routeToPath"];
test("simple non parameterized path", () => {
expect(routeToPath({path: "/abc"} as Route, {})).toBe("/abc");
expect(routeToPath({path: "/abc/def"} as Route, {})).toBe("/abc/def");
expect(routeToPath({path: "/abc"} as Route, { val: 12 })).toBe("/abc");
});
test("simple parameterized path", () => {
expect(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", () => {
const getRouteParams = Router.prototype["getRouteParams"];
test("properly match simple routes", () => {
// simple route
expect(getRouteParams({path: "/home"} as Route, "/home")).toEqual({});
// no match
expect(getRouteParams({path: "/home"} as Route, "/otherpath")).toEqual(false);
// fallback route
expect(getRouteParams({path: "*"} as Route, "somepath")).toEqual({});
});
test("match some parameterized routes", () => {
expect(getRouteParams({path: "/invoices/{{id}}"} as Route, "/invoices/3")).toEqual({
id: "3"
});
});
test("can convert to number if needed", () => {
expect(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");
});
});
-953
View File
@@ -1,953 +0,0 @@
import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store";
import {
makeTestFixture,
makeTestEnv,
nextMicroTick,
nextTick
} from "./helpers";
describe("basic use", () => {
test("commit a mutation", () => {
const state = { n: 1 };
const mutations = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, mutations });
expect(store.state.n).toBe(1);
store.commit("inc", 14);
expect(store.state.n).toBe(15);
});
test("dispatch an action", () => {
const state = { n: 1 };
const mutations = {
inc({ state }, delta) {
state.n += delta;
}
};
const actions = {
inc({ commit }, delta) {
commit("inc", delta);
}
};
const store = new Store({ state, mutations, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc", 14);
expect(store.state.n).toBe(15);
});
test("modifying state outside of mutations trigger error", () => {
const state = { n: 1 };
const actions = {
inc({ state }) {
state.n++;
}
};
const store = new Store({ state, mutations: {}, actions });
expect(() => store.dispatch("inc")).toThrow();
expect(() => (store.state.n = 15)).toThrow();
});
test("can dispatch an action in an action", () => {
const state = { n: 1 };
const mutations = {
inc({ state }, delta) {
state.n += delta;
}
};
const actions = {
inc({ commit }, delta) {
commit("inc", delta);
},
inc100({ dispatch }) {
dispatch("inc", 100);
}
};
const store = new Store({ state, mutations, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc100");
expect(store.state.n).toBe(101);
});
test("can commit a mutation in a mutation", () => {
const state = { n: 1 };
const mutations = {
inc({ state }) {
state.n++;
},
inc10({ commit }) {
for (let i = 0; i < 10; i++) {
commit("inc");
}
}
};
const store = new Store({ state, mutations });
expect(store.state.n).toBe(1);
store.commit("inc10");
expect(store.state.n).toBe(11);
});
test("return data from committing a mutation", () => {
const state = { n: 1 };
const mutations = {
inc({ state }) {
return ++state.n;
}
};
const store = new Store({ state, mutations });
expect(store.state.n).toBe(1);
const res = store.commit("inc");
expect(store.state.n).toBe(2);
expect(res).toBe(2);
});
test("dispatch allow synchronizing between actions", async () => {
const state = { n: 1 };
const mutations = {
inc({ state }, delta) {
state.n += delta;
},
setN({ state }, n) {
state.n = n;
}
};
const actions = {
async dosomething({ commit, dispatch }) {
await dispatch("setTo10");
commit("inc", 3);
},
async setTo10({ commit }) {
await Promise.resolve();
commit("setN", 10);
}
};
const store = new Store({ state, mutations, 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("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("set function is given to mutations", async () => {
let updateCounter = 0;
const state = { bertinchamps: "brune" };
const mutations = {
addInfo({ state, set }) {
set(state, "chouffe", "blonde");
}
};
const store = new Store({ state, mutations, actions: {} });
store.on("update", null, () => updateCounter++);
expect(updateCounter).toBe(0);
store.commit("addInfo");
await nextMicroTick();
expect(updateCounter).toBe(1);
expect(store.state).toEqual({ bertinchamps: "brune", chouffe: "blonde" });
});
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 = {
beerTasterName({ state }, beerID) {
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
return state.beers[1].name;
}
};
const store = new Store({ state, mutations: {}, 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, mutations: {}, actions, getters });
store.dispatch("action");
});
test("getters given to mutations", 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 mutations = {
mutation({ getters }) {
expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron");
}
};
const store = new Store({ state, mutations, actions: {}, getters });
store.commit("mutation");
});
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 });
expect(store.getters.a()).toBe("bc1");
});
});
describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => {
const state = {};
const mutations = {
donothing() {}
};
const store = new Store({ state, mutations });
expect(store.state).toBe(state);
store.commit("donothing");
expect(store.state).toBe(state);
});
test("can use array properties in mutations", () => {
expect.assertions(3);
const state = { a: [1, 2, 3] };
const mutations = {
m({ state }) {
expect(state.a.length).toBe(3);
const l = state.a.push(53);
expect(l).toBe(4);
}
};
const store = new Store({ state, mutations });
store.commit("m");
expect(store.state.a).toEqual([1, 2, 3, 53]);
});
test("can use object assign in store", async () => {
const mutations = {
dosomething({ state }) {
Object.assign(state.westmalle, { a: 3, b: 4 });
}
};
const store = new Store({
state: { westmalle: { a: 1, b: 2 } },
mutations
});
store.commit("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
});
test("aku reactive store state 1", async () => {
const mutations = {
inc({ state }) {
state.counter++;
}
};
const state = { counter: 0 };
const store = new Store({ state, mutations });
expect(store.state.counter).toBe(0);
store.commit("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 mutations = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, mutations });
store.on("update", null, () => updateCounter++);
store.commit("inc", 14);
expect(updateCounter).toBe(0);
store.commit("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 mutations = {
inc({ state }, delta) {
state.n += delta;
},
noop() {},
noop2({ state }) {
const val = state.n;
state.n = val;
}
};
const store = new Store({ state, mutations });
store.on("update", null, () => updateCounter++);
store.commit("noop");
await nextMicroTick();
expect(updateCounter).toBe(0);
store.commit("inc", 50);
await nextMicroTick();
expect(updateCounter).toBe(1);
store.commit("noop2");
await nextMicroTick();
expect(updateCounter).toBe(1);
});
});
describe("connecting a component to store", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
test("connecting a component works", async () => {
env.qweb.addTemplate(
"App",
`
<div>
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/>
</t>
</div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
widgets = { Todo };
}
class Todo extends Component<any, any, any> {}
const state = { todos: [] };
const mutations = {
addTodo({ state }, msg) {
state.todos.push({ msg });
}
};
function mapStateToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(mapStateToProps)(App);
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
store.commit("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
test("deep and shallow connecting a component", async () => {
const state = { todos: [{ title: "Kasteel" }] };
const mutations = {
edit({ state }, title) {
state.todos[0].title = title;
}
};
function mapStateToProps(s) {
return { todos: s.todos };
}
const store = new Store({ state, mutations });
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>`
);
class App extends Component<any, any, any> {}
const DeepTodoApp = connect(
mapStateToProps,
{ deep: true }
)(App);
const ShallowTodoApp = connect(
mapStateToProps,
{ deep: false }
)(App);
(<any>env).store = store;
const deepTodoApp = new DeepTodoApp(env);
const shallowTodoApp = new ShallowTodoApp(env);
await deepTodoApp.mount(fixture);
const shallowFix = makeTestFixture();
await shallowTodoApp.mount(shallowFix);
expect(fixture.innerHTML).toMatchSnapshot();
expect(shallowFix.innerHTML).toMatchSnapshot();
store.commit("edit", "Bertinchamps");
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
expect(shallowFix.innerHTML).toMatchSnapshot();
});
test("connected child components with custom hooks", async () => {
let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> {
mounted() {
steps.push("child:mounted");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
const ConnectedChild = connect(s => s)(Child);
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
</div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild };
constructor(env: Env) {
super(env);
this.state = { child: true };
}
}
const store = new Store({ state: {} });
(<any>env).store = store;
const parent = new Parent(env);
await parent.mount(fixture);
expect(steps).toEqual(["child:mounted"]);
parent.state.child = false;
await nextTick();
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
});
test("connect receives ownprops as second argument", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }] };
let nextId = 2;
const mutations = {
addTodo({ state }, text) {
state.todos.push({ text, id: nextId++ });
}
};
const store = new Store({ state, mutations });
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id);
return todo;
})(TodoItem);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
function mapStateToProps(state) {
return { todos: state.todos };
}
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.commit("addTodo", "hoegaarden");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><span>jupiler</span><span>hoegaarden</span></div>"
);
});
test("connect receives store getters as third argument", async () => {
const state = {
importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
};
const getters = {
importantTodoText({ state }) {
return state.todos.find(todo => todo.id === state.importantID).text;
},
text({ state }, id) {
return state.todos.find(todo => todo.id === id).text;
}
};
const store = new Store({ state, getters });
env.qweb.addTemplate(
"TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>`
);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => {
const todo = state.todos.find(t => t.id === props.id);
return {
activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText()
};
})(TodoItem);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
function mapStateToProps(state) {
return { todos: state.todos };
}
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
);
});
test("connected component is updated when props are updated", async () => {
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return state.beers[props.id];
})(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 1 };
}
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
const store = new Store({ state });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
app.state.beerId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
});
test("connected component is updated when store is changed", async () => {
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>`
);
class App extends Component<any, any, any> {}
const mutations = {
addBeer({ state }, name) {
state.beers.push({ name });
}
};
const state = { beers: [{ name: "jupiler" }] };
const store = new Store({ state, mutations });
(<any>env).store = store;
function mapStateToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
const ConnectedApp = connect(mapStateToProps)(App);
const app = new ConnectedApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.commit("addBeer", "kwak");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><span>jupiler</span><span>kwak</span></div>"
);
});
test("connected component with undefined, null and string props", async () => {
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
};
})(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 0 };
}
const mutations = {
consume({ state }, beerId) {
state.consumedID = beerId;
}
};
const state = {
beers: {
1: { name: "jupiler" }
},
consumedID: null,
taster: "aaron"
};
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span></div></div>"
);
app.state.beerId = 1;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
);
store.commit("consume", 1);
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
);
app.state.beerId = 0;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
);
});
test("connected component deeply reactive with undefined, null and string props", async () => {
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
};
})(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 0 };
}
const mutations = {
changeTaster({ state }, newTaster) {
state.taster = newTaster;
},
consume({ state }, beerId) {
state.consumedID = beerId;
},
renameBeer({ state }, { beerId, name }) {
state.beers[beerId].name = name;
}
};
const state = {
beers: {
1: { name: "jupiler" }
},
consumedID: null,
taster: "aaron"
};
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span></div></div>"
);
app.state.beerId = 1;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
);
store.commit("renameBeer", { beerId: 1, name: "kwak" });
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
);
store.commit("consume", 1);
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
);
app.state.beerId = 0;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
);
store.commit("renameBeer", { beerId: 1, name: "jupiler" });
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
);
store.commit("changeTaster", "matthieu");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
);
});
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-widget="Child" t-props="{key: props.current}"/>
</div>
`
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild };
}
const ConnectedParent = connect(function(s) {
steps.push("parent");
return { current: s.current, isvisible: s.isvisible };
})(Parent);
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
class Child extends Component<any, any, any> {}
const ConnectedChild = connect(function(s, props) {
steps.push("child");
return { msg: s.msg[props.key] };
})(Child);
const state = { current: "a", msg: { a: "a", b: "b" } };
const mutations = {
setCurrent({ state }, c) {
state.current = c;
}
};
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new ConnectedParent(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
expect(steps).toEqual(["parent", "child"]);
store.commit("setCurrent", "b");
await nextTick();
expect(steps).toEqual(["parent", "child", "parent", "child", "child"]);
});
test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> {
willPatch() {
steps.push("willpatch");
}
patched() {
steps.push("patched");
}
}
const ConnectedApp = connect(function(s) {
return { msg: s.msg };
})(App);
const state = { msg: "a" };
const mutations = {
setMsg({ state }, c) {
state.msg = c;
}
};
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new ConnectedApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
store.commit("setMsg", "b");
await nextTick();
expect(fixture.innerHTML).toBe("<div>b</div>");
expect(steps).toEqual(["willpatch", "patched"]);
});
});
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></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);
});
});
+30 -144
View File
@@ -1,4 +1,5 @@
import { h, patch, init } from "../src/vdom";
import { h, patch } from "../src/vdom";
import { init } from "../src/vdom/vdom";
function map(list, fn) {
var ret: any[] = [];
@@ -59,9 +60,7 @@ describe("attributes", function() {
test("are set correctly when namespaced", function() {
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
elm = patch(vnode0, vnode1).elm;
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe(
"#foo"
);
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
});
test("should not touch class nor id fields", function() {
@@ -207,12 +206,8 @@ 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;
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
.elm;
expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
@@ -352,13 +347,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(2);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("add elements in the middle", function() {
@@ -368,13 +357,7 @@ describe("snabbdom", function() {
expect(elm.children.length).toBe(4);
expect(elm.children.length).toBe(4);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("add elements at beginning and end", function() {
@@ -383,13 +366,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(3);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("adds children to parent with no children", function() {
@@ -588,35 +565,19 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"4",
"3",
"2",
"1",
"5",
"0"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
});
test("supports null/undefined children", function() {
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
const vnode2 = h(
"i",
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
spanNum
)
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
);
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"2",
"1",
"0",
"5",
"4",
"3"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
});
test("supports all null/undefined children", function() {
@@ -627,14 +588,7 @@ describe("snabbdom", function() {
elm = patch(vnode1, vnode2).elm;
expect(elm.children.length).toBe(0);
elm = patch(vnode2, vnode3).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"5",
"4",
"3",
"2",
"1",
"0"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
});
});
});
@@ -714,18 +668,10 @@ describe("snabbdom", function() {
});
test("removes elements", function() {
const vnode1 = h("div", [
h("span", "One"),
h("span", "Two"),
h("span", "Three")
]);
const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"One",
"Two",
"Three"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
});
@@ -760,49 +706,19 @@ describe("snabbdom", function() {
});
test("reorders elements", function() {
const vnode1 = h("div", [
h("span", "One"),
h("div", "Two"),
h("b", "Three")
]);
const vnode2 = h("div", [
h("b", "Three"),
h("span", "One"),
h("div", "Two")
]);
const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"One",
"Two",
"Three"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
expect(map(elm.children, c => c.innerHTML)).toEqual([
"Three",
"One",
"Two"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
});
test("supports null/undefined children", function() {
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
const vnode2 = h("i", [
h("i", "2"),
undefined,
undefined,
h("i", "1"),
undefined
]);
const vnode3 = h("i", [
null,
h("i", "1"),
undefined,
null,
h("i", "2"),
undefined,
null
]);
const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
elm = patch(vnode1, vnode2).elm;
@@ -835,10 +751,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { create: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
]),
h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Can't touch me")
]);
patch(vnode0, vnode1);
@@ -855,10 +768,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { insert: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
]),
h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Can touch me")
]);
patch(vnode0, vnode1);
@@ -874,17 +784,11 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div", [
h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -965,10 +869,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { remove: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div", [h("span", "First sibling")]);
patch(vnode0, vnode1);
@@ -981,9 +882,7 @@ describe("snabbdom", function() {
function cb(vnode) {
calls++;
}
const vnode1 = h("div", [
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
]);
const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
const vnode2 = h("div", "Text node");
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1053,10 +952,7 @@ describe("snabbdom", function() {
result.push(vnode);
rm();
}
const vnode1 = h("div", { hook: { remove: cb } }, [
h("b", "Child 1"),
h("i", "Child 2")
]);
const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1091,10 +987,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", [
h("span", { hook: { destroy: cb } }, "Child 1"),
h("span", "Child 2")
])
h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div");
patch(vnode0, vnode1);
@@ -1150,11 +1043,7 @@ describe("snabbdom", function() {
}
}
]);
const vnode1 = h("div", [
h("span", "First child"),
"",
h("span", "Third child")
]);
const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
const vnode2 = h("div");
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1196,10 +1085,7 @@ describe("snabbdom", function() {
function cb(vnode) {
result.push(vnode);
}
const vnode1 = h("div", [
h("span", { hook: { update: cb } }, "Hello"),
h("span", "there")
]);
const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
patch(vnode0, vnode1);
patch(vnode1, vnode1);
expect(result).toHaveLength(0);
@@ -1,4 +1,9 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
odoo.define("app", function(require) {
const Widget = require("web.Widget");
@@ -97,14 +102,23 @@ odoo.define("app", function(require) {
"click .o_btn_msg.10000": function() {
this.addMessages(10000);
},
"click .o_btn_msg.50000": function() {
this.addMessages(50000);
"click .o_btn_msg.30000": function() {
this.addMessages(30000);
},
"click .updateSomeMessages": function() {
this.updateSomeMessages();
},
"click .clear": function() {
this.clear();
},
"click .o_multiple": function() {
this.multipleFlag = !this.multipleFlag;
},
"click .o_clear": function() {
this.clearFlag = !this.clearFlag;
},
"click .clear-log": function() {
this.$log[0].innerHTML = "";
}
},
custom_events: {
@@ -116,11 +130,15 @@ odoo.define("app", function(require) {
this.widgets = {};
this.isInDom = false;
this.messageCount = 0;
this.multipleFlag = false;
this.clearFlag = false;
},
start: function() {
this.$content = this.$(".content");
this.$msgCount = this.$(".message_count");
this.$log = this.$(".log-content");
this.log("Benchmarking odoo widgets, 12.0");
},
on_attach_callback: function() {
this.isInDom = true;
@@ -144,34 +162,41 @@ odoo.define("app", function(require) {
},
addMessages: function(n) {
var self = this;
startMeasure("add " + n);
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
$.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
stopMeasure();
});
const self = this;
this.benchmark("add " + n, () => {
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
return $.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
});
});
},
clear: function() {
startMeasure("clear");
this._clear();
stopMeasure(info => {
this.log(info.msg);
});
},
_clear: function() {
this.$content.empty();
for (let key in this.widgets) {
this.widgets[key].destroy();
@@ -179,15 +204,14 @@ odoo.define("app", function(require) {
}
this.messageCount = 0;
this.updateMessageCount();
stopMeasure();
},
updateSomeMessages: function() {
startMeasure("update every 10th");
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
stopMeasure();
this.benchmark("update every 10th", () => {
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
});
},
_onRemoveMessage: function(ev) {
startMeasure("remove message");
@@ -196,6 +220,65 @@ odoo.define("app", function(require) {
this.messageCount--;
this.updateMessageCount();
stopMeasure();
},
benchmark: function(message, fn, callback) {
if (this.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.clearFlag) {
this._benchmark("clear", this._clear.bind(this), finalize, false);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
},
_benchmark: function(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
const benchmark = fn();
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
});
},
log: function(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.$log[0].appendChild(div);
this.$log[0].scrollTop = this.$log[0].scrollHeight;
}
});
@@ -0,0 +1,47 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1k messages</button>
<button class="o_btn_msg 10000">Add 10k messages</button>
<button class="o_btn_msg 30000">Add 30k messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" class="o_multiple" id="multipleflag" />
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" class="o_clear" id="clearFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info message_count">
Number of messages: 0
</div>
<div class="title">Log <span class="clear-log">(clear)</span></div>
<div class="log">
<div class="log-content"/>
</div>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove">Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
@@ -1,4 +1,9 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
odoo.define("app", function(require) {
const Widget = require("web.Widget");
@@ -97,14 +102,23 @@ odoo.define("app", function(require) {
"click .o_btn_msg.10000": function() {
this.addMessages(10000);
},
"click .o_btn_msg.50000": function() {
this.addMessages(50000);
"click .o_btn_msg.30000": function() {
this.addMessages(30000);
},
"click .updateSomeMessages": function() {
this.updateSomeMessages();
},
"click .clear": function() {
this.clear();
},
"click .o_multiple": function() {
this.multipleFlag = !this.multipleFlag;
},
"click .o_clear": function() {
this.clearFlag = !this.clearFlag;
},
"click .clear-log": function() {
this.$log[0].innerHTML = "";
}
},
custom_events: {
@@ -116,11 +130,15 @@ odoo.define("app", function(require) {
this.widgets = {};
this.isInDom = false;
this.messageCount = 0;
this.multipleFlag = false;
this.clearFlag = false;
},
start: function() {
this.$content = this.$(".content");
this.$msgCount = this.$(".message_count");
this.$log = this.$(".log-content");
this.log("Benchmarking odoo widgets, 12.3");
},
on_attach_callback: function() {
this.isInDom = true;
@@ -144,34 +162,41 @@ odoo.define("app", function(require) {
},
addMessages: function(n) {
var self = this;
startMeasure("add " + n);
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
$.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
stopMeasure();
});
const self = this;
this.benchmark("add " + n, () => {
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
return $.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
});
});
},
clear: function() {
startMeasure("clear");
this._clear();
stopMeasure(info => {
this.log(info.msg);
});
},
_clear: function() {
this.$content.empty();
for (let key in this.widgets) {
this.widgets[key].destroy();
@@ -179,15 +204,14 @@ odoo.define("app", function(require) {
}
this.messageCount = 0;
this.updateMessageCount();
stopMeasure();
},
updateSomeMessages: function() {
startMeasure("update every 10th");
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
stopMeasure();
this.benchmark("update every 10th", () => {
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
});
},
_onRemoveMessage: function(ev) {
startMeasure("remove message");
@@ -196,6 +220,65 @@ odoo.define("app", function(require) {
this.messageCount--;
this.updateMessageCount();
stopMeasure();
},
benchmark: function(message, fn, callback) {
if (this.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.clearFlag) {
this._benchmark("clear", this._clear.bind(this), finalize, false);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
},
_benchmark: function(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
const benchmark = fn();
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
});
},
log: function(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.$log[0].appendChild(div);
this.$log[0].scrollTop = this.$log[0].scrollHeight;
}
});
@@ -0,0 +1,47 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1k messages</button>
<button class="o_btn_msg 10000">Add 10k messages</button>
<button class="o_btn_msg 30000">Add 30k messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" class="o_multiple" id="multipleflag" />
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" class="o_clear" id="clearFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info message_count">
Number of messages: 0
</div>
<div class="title">Log <span class="clear-log">(clear)</span></div>
<div class="log">
<div class="log-content"/>
</div>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove">Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
+173
View File
@@ -0,0 +1,173 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();

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