Compare commits

...

374 Commits

Author SHA1 Message Date
Géry Debongnie b0e2ef82f7 [REL] bump to v0.24.1 2019-10-28 14:58:52 +01:00
Aaron Bohy c9c2b3fa6d [IMP] tests: add concurrency test 2019-10-28 14:46:21 +01:00
Géry Debongnie 62608cbb0d [DOC] add architecture notes on rendering pipeline 2019-10-28 14:23:02 +01:00
Géry Debongnie 3035a9f009 [DOC] move reference doc in subfolder 2019-10-28 14:23:02 +01:00
Géry Debongnie c2adb429bd [FIX] package.json: update node and typescript version
closes #414
2019-10-28 13:58:44 +01:00
Géry Debongnie da9dda5eca [DOC] component: add more information on t-key
closes #353
2019-10-28 11:45:51 +01:00
Géry Debongnie 5614c85b04 [IMP] qweb: implement t-key with a directive
closes #331
2019-10-28 11:45:51 +01:00
Aaron Bohy 5d57a7bf13 [FIX] component: updateProps: validation and default values
Before this rev., default values weren't taken into account when
validating props whenever a component was updated. Moreover, there
was no test attesting that props were validated at update.
2019-10-28 11:32:43 +01:00
Géry Debongnie 4ebe419c56 [FIX] component: make sure props are validated in all cases
closes #379
2019-10-28 11:32:43 +01:00
Géry Debongnie 9779cd196c [REF] fiber: small cleanups 2019-10-28 11:32:43 +01:00
Géry Debongnie 9cee12d7b4 [IMP] component: propagate error to mount and render 2019-10-28 11:32:43 +01:00
Géry Debongnie 2c563ee380 [REF] component: move error handling into the fiber 2019-10-28 11:32:43 +01:00
Géry Debongnie 2aa705f5c8 [FIX] router: preserve pathname in hash mode
closes #397
2019-10-28 11:32:43 +01:00
Géry Debongnie 5911c8e3f6 [DOC] store,hooks: useState/useStore should return object
or array

closes #404
2019-10-28 11:32:43 +01:00
Géry Debongnie 2279dafbec [IMP] qweb: add an option to setup a translate function
closes #393
2019-10-28 11:32:41 +01:00
Géry Debongnie f07ec21a07 [FIX] playground: add missing ;
Because of that missing ";", the playground application tried to inject
this:

window.TEMPLATES = `...`

instead of this:

window.TEMPLATES = `...`;

So, when the following line started with a (, then the javascript code
was interpreted as a function call. And it could happen, because some
people like to use the IIFE syntax to have some contained code.
2019-10-26 09:34:51 +02:00
Géry Debongnie b5b6b7342e [DOC] improve main readme.md 2019-10-26 09:27:35 +02:00
Géry Debongnie 97b914b9cd [IMP] playground: log url for current owl commit
closes #405
2019-10-26 09:21:04 +02:00
Géry Debongnie 9dfcfda365 [DOC] add more info on xml tag and single file component
closes #401
2019-10-26 09:10:50 +02:00
Géry Debongnie e026f537ae [ADD] tools: add benchmarks for v0.24.0 2019-10-25 17:17:20 +02:00
Géry Debongnie f0b5a55ad1 [REL] bump to v0.24.0 2019-10-25 17:07:51 +02:00
Géry Debongnie 690d8edf67 [FIX] context: solve tricky concurrency issue
part of #330
2019-10-25 16:01:52 +02:00
Géry Debongnie 3c38bbc076 [REF] component: large cleanup of concurrency branch
We remove here old comments, add some tests and documentation, and in
general, make sure the state of the code is in a good shape

part of #330
2019-10-25 16:01:52 +02:00
Aaron Bohy 9c5cad15c1 [IMP] component: refactor rendering pipeline
This commit introduces a brand new rendering system based on a fiber
class and a scheduler.

closes #330
2019-10-25 16:01:52 +02:00
Géry Debongnie 2bed1cfbd1 [REF] asyncroot: create asyncroot component
This component replaces the t-asyncroot directive.
2019-10-25 16:01:52 +02:00
Géry Debongnie caa9ac5cb2 [ADD] project: add prettier task to package.json 2019-10-25 16:01:52 +02:00
Géry Debongnie 67349eb0f0 [IMP] vdom: remove handling of combined selectors
this is done by qweb anyway
2019-10-25 16:01:52 +02:00
Géry Debongnie 1ca401811f [DOC] add example of inline statement in main doc 2019-10-25 14:56:09 +02:00
Aaron Bohy 2beb12678e [IMP] t-on directive: allow empty handler
Closes #377
2019-10-25 14:11:44 +02:00
Aaron Bohy 0ea1091692 [IMP] t-on directive: handle inline statements
Closes #265
2019-10-25 14:11:44 +02:00
Nicolas Bayet 71827c3ba8 [DOC] explain router history and hash mode 2019-10-25 13:36:18 +02:00
Géry Debongnie c2f42d3b82 [FIX] qweb: do not format expression twice in t-if
closes #392
2019-10-24 14:57:48 +02:00
Géry Debongnie a3317ab997 [FIX] qweb: handle variable expressions in t-if
closes #390
closes #362
2019-10-24 08:55:30 +02:00
Géry Debongnie 403935a41e [DOC] qweb: document t-att variant
closes #378
2019-10-22 22:03:10 +02:00
Géry Debongnie 39af9ec938 [DOC] improve store documentation on useGetters
closes #386
2019-10-22 22:00:41 +02:00
Géry Debongnie 13128ed425 [FIX] component: do not validate props twice
it is not useful to do it in the component directive, especially since
it is done in the constructor, and the default props are not applied.

closes #379
2019-10-22 21:58:51 +02:00
Géry Debongnie be556a970e [FIX] qweb: properly calls directive finalizers
closes #382
2019-10-22 21:56:29 +02:00
Aaron Bohy da6c24bbca [FIX] test: do not check external links in doc 2019-10-21 09:47:20 +02:00
Géry Debongnie f6d6da8393 [FIX] component: fix issues with component internal template key
closes #298
2019-10-21 09:41:27 +02:00
Géry Debongnie 918945c11e [DOC] improve the comparison documentation page
closes #365
closes #366
2019-10-20 14:46:01 +02:00
Géry Debongnie 29c2c5b9c9 [DOC] improve the documentation
closes #340
closes #344
closes #352
closes #355
2019-10-20 09:17:52 +02:00
Aaron Bohy 1bb4577ec1 [DOC] improve hooks and qweb doc
Closes #358
Closes #359
2019-10-18 09:42:03 +02:00
Géry Debongnie 0aeebd7b6e [REF] utils: rename loadTemplates into loadFile
closes #351
2019-10-18 08:05:26 +02:00
Géry Debongnie 4bc49e7241 [DOC] add a roadmap
closes #347
2019-10-17 22:36:44 +02:00
Géry Debongnie 2b5783d9bd [FIX] playground: update samples to make them work on firefox
Sadly, Firefox does not support yet static class fields, so we need to
use an equivalent, but slightly not as nice, syntax.

closes #335
2019-10-17 16:57:40 +02:00
Géry Debongnie 1f40f113aa [DOC] update the documentation
closes #334
closes #336
closes #338
closes #339
closes #341
closes #342
closes #343
closes #346
closes #348
2019-10-17 12:08:24 +02:00
Géry Debongnie 9e65b0686f [REL] bump to v0.23.0 2019-10-16 22:31:04 +02:00
Géry Debongnie 1d72164015 [FIX] component: support component reduced to a slot 2019-10-16 21:48:49 +02:00
Géry Debongnie 2825a5a55d [FIX] qweb: properly handle xml comments 2019-10-15 15:47:50 +02:00
Géry Debongnie d40ff53f5a [DOC] doc: add more information in store section 2019-10-14 15:00:31 +02:00
Aaron Bohy 23246c42aa [REF] *: snakecase filenames 2019-10-14 11:06:59 +02:00
Géry Debongnie 895fe7c60f [REF] store: replace ConnectedComponent by useStore hook
Closes #304
2019-10-14 11:06:59 +02:00
Géry Debongnie 19b10f7c1d [IMP] playground: update todoapp sample 2019-10-14 11:06:59 +02:00
Géry Debongnie 0788d9fe75 [REF] qweb: rename Context into CompilationContext 2019-10-14 11:06:59 +02:00
Géry Debongnie 4a346d0615 [FIX] qweb: properly remove t-ref reference
when the node is removed from the dom
2019-10-14 11:06:59 +02:00
Géry Debongnie 1337aa0107 [IMP] component: add current key for easier hook creation
closes #319
2019-10-14 10:25:34 +02:00
Géry Debongnie 3e8f60cefa [FIX] qweb: properly handle t-raw in various situations
closes #325
closes #327
2019-10-14 09:59:26 +02:00
Géry Debongnie 41b0618c93 [FIX] qweb: improve generated compiled code 2019-10-11 14:10:44 +02:00
Aaron Bohy 0928c189f4 [FIX] qweb: allow to combine t-esc with other directives
With this rev., when a t-esc is not set on a <t> tag, we create
it inside the node on which it is set, to ensure that the t-esc
directive is always set on <t> tags.

Same applies for t-raw.

Closes #324
2019-10-11 14:10:44 +02:00
Aaron Bohy 2f27768be2 [FIX] qweb: event modifier with arg in t-foreach
Closes #266
2019-10-11 14:01:07 +02:00
Aaron Bohy aa1b0f502d [FIX] tests: fix import 2019-10-11 14:01:07 +02:00
Géry Debongnie da1824f4dd [DOC] doc: improve navigation to main doc page
also, fixes a small error
2019-10-10 12:28:36 +02:00
Géry Debongnie 07dac970f7 [IMP[ component/hooks: add onWillStart and onWillUpdateProps 2019-10-09 15:09:26 +02:00
Géry Debongnie 5f6f081ae2 [IMP] playground: save changes to local storage
closes #316
closes #101
2019-10-09 10:15:09 +02:00
Géry Debongnie 1c3c2e5c51 [IMP] hooks/Context: add Context and useContext
Closes #310
2019-10-08 15:56:46 +02:00
Aaron Bohy 17ce73e6a9 [REF] hooks: improve makeLifecycleHook 2019-10-08 15:52:44 +02:00
Géry Debongnie cedb1f411f [REF] component: move template check in constructor 2019-10-08 13:51:55 +02:00
Géry Debongnie e7516be95c [DOC] component: remove doc for removed updateEnv method 2019-10-07 16:07:56 +02:00
Géry Debongnie 683993dbc4 [FIX] component/hooks: onWillUnmount was not properly called 2019-10-07 16:01:28 +02:00
Géry Debongnie 50d569411c [DOC] hooks: fixing broken link 2019-10-07 15:51:45 +02:00
Géry Debongnie c3453d35b9 [IMP] hooks/component: remove updateEnv, add useSubEnv
closes #182
2019-10-04 22:54:27 +02:00
Géry Debongnie b0dd0252a4 [REF] component: move method at proper place 2019-10-04 22:54:27 +02:00
Géry Debongnie e2b2e54e29 [DOC] tooling: add missing link 2019-10-04 22:12:56 +02:00
Géry Debongnie 766c378ac9 [DOC] hooks: add missing doc for onWillPatch/onPatched 2019-10-03 11:57:58 +02:00
Géry Debongnie 022b29b6a0 [IMP] component/qweb: remove t-mounted directive
The t-mounted directive main goals can be achieved with hooks, in a
better and more intuitive way.

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

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

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

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

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

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

This is a pretty big change toward single file owl components

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

closes #223
2019-06-28 16:53:14 +02:00
Géry Debongnie 1c8e1af86d [IMP] tools: improve error message display 2019-06-28 14:54:24 +02:00
Mathieu Duckerts-Antoine 60a6cca960 [IMP] qweb: better error messages for bad xml templates
closes #185
2019-06-28 14:53:19 +02:00
Géry Debongnie ad55b42ccb [IMP] playground: improve first example
Previous example did not illustrate props
2019-06-28 14:31:19 +02:00
Géry Debongnie 50c0a4b126 [DOC] improve component documentation 2019-06-28 13:00:38 +02:00
Géry Debongnie 2ca42e7470 [ADD] tools: add v0.16.0 benchmark 2019-06-28 11:51:37 +02:00
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
Géry Debongnie f60904b740 [REL] bump to v0.12.0 2019-05-29 11:26:21 +02:00
Aaron Bohy 376f801541 [IMP] qweb: t-widget: accept dyn values 2019-05-29 11:22:12 +02:00
Aaron Bohy 26f14180e2 [IMP] qweb: t-ref: dyn values with string interpolation
Closes #100
2019-05-29 11:22:12 +02:00
Géry Debongnie bced9f3d04 [IMP] *: use lower level loops in hot code
The goal is to gain a few % of perf improvement.

closes #125
2019-05-29 11:20:30 +02:00
Géry Debongnie 94208d9cbb [IMP] extras: add some css 2019-05-29 10:50:29 +02:00
Géry Debongnie 681983142d [IMP] component: add props validation and default props system
closes #116
2019-05-28 11:56:22 +02:00
Géry Debongnie a525b42537 [IMP] playground: improve code samples
closes #126
2019-05-27 13:33:26 +02:00
Géry Debongnie a4cc085b6d [DOC] qweb: add note to explain t-transition limitation 2019-05-27 12:46:20 +02:00
Aaron Bohy 837dacf1cb [DOC] animations: correct link 2019-05-27 12:10:33 +02:00
Aaron Bohy 7cdd1aa9a1 [IMP] qweb: add global Component registry to qweb
Closes #112
2019-05-27 12:10:33 +02:00
Géry Debongnie 24c569e6d5 [CLEANUP] owl: remove useless dependencies (package.json) 2019-05-27 12:10:30 +02:00
Aaron Bohy 050694414c [REF] move animations tests into their own file 2019-05-27 11:50:21 +02:00
Aaron Bohy 639908366a [IMP] allow to combine t-transition and t-widget
Closes #94
2019-05-27 11:50:21 +02:00
Aaron Bohy 59f401303a [REF] transitions: remove create hook 2019-05-27 11:50:20 +02:00
Aaron Bohy 649e20b956 [IMP] improve tests of transition directive 2019-05-27 11:49:09 +02:00
Géry Debongnie c2280ff1bb [REF] qweb: small cleanup 2019-05-23 23:33:15 +02:00
Aaron Bohy 8a32d823cd [IMP] support modifiers with t-on directive
Supported modifiers are stop, prevent and self.

Closes #115.
2019-05-23 23:33:09 +02:00
Géry Debongnie 13b31458da [DOC] owl: add message when setting mode in 'prod' 2019-05-23 13:44:14 +02:00
Géry Debongnie 92b3ab32e2 [REM] playground: remove benchmarks app
It is kind of redundant with the main benchmarks app, does not bring
much extra value, and is out of date.
2019-05-22 12:29:53 +02:00
Géry Debongnie 4ca0a157e3 [IMP] playground: set owl mode to 'dev'; 2019-05-21 18:08:32 +02:00
Géry Debongnie 1dec8df715 [IMP] owl: introduce dev mode
This commit adds a static key 'dev' on QWeb (which acts as a central
point of access in owl internal code), and a 'mode' key on the value
exported by owl to let external code manipulate it.

closes #117
2019-05-21 14:11:40 +02:00
Géry Debongnie 8f29cfc996 [FIX] playground: add missing t-key in template 2019-05-21 14:11:20 +02:00
Géry Debongnie da275e74fd [REF] owl: move metadata into __info__
This differentiate better metadata and owl exported values.  Also, and
this is the goal, it gives a space to add extra keys in the future.
2019-05-21 13:53:16 +02:00
Géry Debongnie 3d9f4e028c [DOC] utils: remove patch/unpatch references 2019-05-19 08:21:22 +02:00
Géry Debongnie 13577997bc [ADD] extras: add owl 0.11.0 to list of benchmarks 2019-05-17 23:46:42 +02:00
Géry Debongnie 671965026d [REL] bump to v0.11.0 2019-05-17 23:34:14 +02:00
Géry Debongnie bc7fe9023d [REM] utils: remove patch/unpatch
They are out of the scope for Owl.
2019-05-17 23:26:42 +02:00
Géry Debongnie c2c7997df3 [IMP] owl: add extras:watch task
closes #118
2019-05-17 23:22:54 +02:00
Géry Debongnie a1d7e6c987 [FIX] qweb: support #{} syntax in attribute string interpolation
closes #113
2019-05-17 16:20:25 +02:00
Géry Debongnie a1e982aba5 [REF] tests: rename makeTestWEnv to makeTestEnv 2019-05-17 15:52:16 +02:00
Géry Debongnie 2ce027a0a6 [REF] component: remove isStarted key 2019-05-17 15:50:55 +02:00
Géry Debongnie 61f21511a5 [DOC] component: add detailed explanation for t-widget 2019-05-17 10:49:04 +02:00
Géry Debongnie 5682577870 [REF] component: yet another simplification for t-widget 2019-05-17 09:59:02 +02:00
Géry Debongnie 25b633cc63 [REF] component: simplify again t-widget code 2019-05-16 16:25:39 +02:00
Géry Debongnie f16b267931 [REF] component: simplify the t-widget directive
a little bit...
2019-05-16 15:29:40 +02:00
Géry Debongnie 30ec952588 [FIX] playground: keep separate sessions for each tab
also, small refactoring

closes #103
2019-05-16 13:00:24 +02:00
Géry Debongnie 46ba77d0cc [REF] playground: use t-foreach to reduce duplication 2019-05-16 12:04:50 +02:00
Géry Debongnie 042e376c2b [IMP] playground: debounce some handlers 2019-05-16 11:58:04 +02:00
Géry Debongnie b9f1af2ad5 [REF] playground: small refactoring 2019-05-16 11:57:52 +02:00
Géry Debongnie 7df0fdcc0a [DOC] qweb: add some documentation on owl qweb extensions 2019-05-16 10:23:18 +02:00
Géry Debongnie 73d743a1c3 [FIX] observer: newly observed object/arrays have higher rev
Some people apparently believe that the framework should magically
solves all problems from the programmers, shielding him from thinking.

It is not possible, and maybe a bad idea, because it will only delay the
inevitable...  If one does weird stuff without knowing what is going on,
someone will get hurt.

However, in this specific case, something can be done.

closes #104
2019-05-15 17:30:01 +02:00
Géry Debongnie 8758dfcedf [IMP] component: better error if cannot find widget
closes #54
2019-05-15 17:17:39 +02:00
Géry Debongnie 0a10bbc023 [IMP] component: remove support for inlineTemplate
closes #93
2019-05-15 15:54:18 +02:00
Géry Debongnie 9c1d369e94 [FIX] qweb/component: fix scoping issue with list of widgets
In a t-foreach loop, there was a bad interaction between variables
defined with the `var` keyword and asynchronous code.

closes #108
2019-05-15 15:42:45 +02:00
Géry Debongnie 9556696fa8 [REF] qweb: rename loadTemplates into addTemplates 2019-05-15 11:04:22 +02:00
Géry Debongnie 04eb720619 [IMP] qweb: add default template in QWeb 2019-05-15 10:55:17 +02:00
Géry Debongnie 8e098e8ed6 [REF] benchmarks: use new system for finding templates 2019-05-15 10:40:23 +02:00
Géry Debongnie c836cead58 [IMP] playground: remove explicit template keys 2019-05-15 10:40:23 +02:00
Géry Debongnie 361ecbe90b [IMP] component: deduce template name from component name
closes #102
2019-05-15 10:40:23 +02:00
Géry Debongnie ae8d7d3be2 [REF] qweb_core: move context after QWeb 2019-05-15 10:40:23 +02:00
Géry Debongnie ed4f92211c [DOC] component: add section on async rendering 2019-05-11 12:11:00 +02:00
Géry Debongnie f8804d6501 [REF] playground: use t-mounted directive in todo example 2019-05-11 00:03:26 +02:00
Géry Debongnie de7c680387 [ADD] extras: add owl 0.10.0 to list of benchmarks 2019-05-11 00:03:26 +02:00
Géry Debongnie 6dd7391cdc [DOC] utils: add some basic information 2019-05-10 21:04:58 +02:00
Géry Debongnie dfa7d5932d [REM] utils: remove memoize, parseXML function 2019-05-10 20:51:34 +02:00
Géry Debongnie 53911de00a [FIX] component: make t-widget work with t-elif
closes #97
2019-05-10 19:36:44 +02:00
Aaron Bohy 1760d0d336 [FIX] allow to combine directives using node hooks
e.g. t-ref and t-transition, or t-mounted and t-transition.
2019-05-10 19:31:09 +02:00
Aaron Bohy 0aba9748c9 [IMP] add t-mounted directive
closes #45
2019-05-10 19:31:09 +02:00
Aaron Bohy 8ee0973dab [REF] autoformat some files 2019-05-10 19:31:09 +02:00
Géry Debongnie e04c5b1c6b [REL] bump to v0.10.0 2019-05-10 12:19:25 +02:00
Géry Debongnie f52a15d7f6 [REF] store: make getters functions instead of properties 2019-05-10 12:10:52 +02:00
Alexandre Kühn 147f6fced7 [IMP] store: getters 2019-05-10 12:10:52 +02:00
Géry Debongnie d928927da8 [DOC] store: add some basic documentation 2019-05-10 12:10:52 +02:00
Géry Debongnie 29cc94d567 [REF] qweb: use nodeId instead of ctx.parentNode 2019-05-10 11:53:24 +02:00
Géry Debongnie 7f33f9b68e [FIX] component: call patch between willpatch and patched
closes #91
2019-05-09 15:22:59 +02:00
Géry Debongnie 80ffa37773 [TODO] fix separate issue. need a test 2019-05-09 13:12:26 +02:00
Géry Debongnie 2f70a7a352 [DOC] component: add a section on semantics
closes #89
2019-05-08 15:25:54 +02:00
Géry Debongnie f7f8c34e1c [DOC] owl: add generic file level documentation 2019-05-08 11:23:48 +02:00
Géry Debongnie edc7d2b635 [IMP] playground: improve responsive example
close #83
2019-05-07 16:26:20 +02:00
Géry Debongnie 7f0b236747 [FIX] store: properly call patch on connected components
A previous refactoring broke it by adding a [] arguments to updateprops.
2019-05-07 14:43:23 +02:00
Géry Debongnie b7860a95b0 [REF] store: typecheck code
closes #87
closes #39
2019-05-07 11:47:09 +02:00
Géry Debongnie f7495dc897 [FIX] store: correct update order for child/parents
Not happy about the test, there was a bug in Discuss that is solved with this
commit, but I could not reproduce it in a test.

closes #85
2019-05-07 11:22:00 +02:00
Géry Debongnie ba506be58d [FIX] component: fix issue with crash caused by async rendering
This may (or may not?) closes #85
2019-05-06 14:23:29 +02:00
Géry Debongnie 91896942dd [FIX] component: prevent rendering if not mounted 2019-05-06 13:45:50 +02:00
Géry Debongnie dfab8c7333 [DOC] component: fix invalid xml in examples 2019-05-06 13:42:53 +02:00
Géry Debongnie bd29e4f761 [REF] qweb: split code into 3 files
closes #84
2019-05-06 13:00:46 +02:00
Géry Debongnie b9591cfecd [IMP] component: add support for dynamic css on t-widget
closes #80
2019-05-06 11:19:18 +02:00
Géry Debongnie 51320204f9 [IMP] component: add static css support for sub components
part of issue #80
2019-05-06 10:22:53 +02:00
Alexandre Kühn ec0bafaa8e [DOC] component: remove misleading order on willUnmount
Some hooks call order are clearly defined: `[willX]` hooks are
called first on parent, then on children, and `[Xed]` are called in
the reverse order: first children, then parent.

This is also true for `willUnmount`: first on parent, then on
children.
2019-05-04 22:54:20 +02:00
Géry Debongnie eb508b7660 [FIX] component: capture ref key in closure
This is what commit d3197e9865 should have
done. Many changes should be done on normal nodes and on components!

closes #77
2019-05-04 22:48:50 +02:00
Géry Debongnie 7c8fc323e1 [DOC] extras: update readme and server.py doc 2019-05-03 16:38:24 +02:00
Géry Debongnie ff2393f5df [DOC] doc: update qweb.md 2019-05-03 16:19:33 +02:00
Géry Debongnie ab2a98426e [IMP] playground: slightly decrease font size 2019-05-03 14:33:34 +02:00
Géry Debongnie 84b3cc42cf [FIX] qweb: make transition code much more robust
part of #66
2019-05-03 14:15:25 +02:00
Géry Debongnie d28b854e8d [IMP] playground: display promise errors in right pane 2019-05-03 13:26:25 +02:00
Géry Debongnie 6c8c69affa [ADD] extras: add benchmarks for 0.9.0 2019-05-03 12:40:12 +02:00
Géry Debongnie faec7d4ad9 [FIX] readme: fix broken links 2019-05-03 12:34:00 +02:00
203 changed files with 67280 additions and 10686 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
+14
View File
@@ -0,0 +1,14 @@
Most of the files are
Copyright (c) 2004-2015 Odoo S.A.
Many files also contain contributions from third
parties. In this case the original copyright of
the contributions can be traced through the
history of the source version control system.
When that is not the case, the files contain a prominent
notice stating the original copyright and applicable
license, or come with their own dedicated COPYRIGHT
and/or LICENSE file.
+2 -5
View File
@@ -1,12 +1,9 @@
For copyright information, please see the COPYRIGHT file.
Odoo is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
OWL is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
(LGPLv3), as included below. Since the LGPL is a set of additional
permissions on top of the GPL, the text of the GPL is included at the
bottom as well.
Some external libraries and contributions bundled with Odoo may be published
Some external libraries and contributions bundled with OWL may be published
under other GPL-compatible licenses. For these, please refer to the relevant
source files and/or license files, in the source code tree.
+226 -44
View File
@@ -1,19 +1,70 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
_A web framework for structured, dynamic and maintainable applications_
_A no nonsense web framework for structured, dynamic and maintainable applications_
## Project Overview
The Odoo Web Library (OWL) is a small
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
The Odoo Web Library (OWL) is a smallish (~18kb gzipped) UI framework intended to
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
- a declarative component system,
- a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router
## OWL's design principles
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
are some code examples to showcase some interesting features.
Owl is currently mostly stable. Possible future changes are explained in the
[roadmap](roadmap.md).
## Example
Here is a short example to illustrate interactive components:
```javascript
const { Component, QWeb, useState } = owl;
const { xml } = owl.tags;
class Counter extends Component {
static template = xml`
<button t-on-click="state.value++">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
}
class App extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter };
}
const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Design Principles
OWL is designed to be used in highly dynamic applications where changing
requirements are common, code needs to be maintained by large teams.
requirements are common and code needs to be maintained by large teams.
- **XML based**: templates are based on the XML format, which allows interesting
applications. For example, they could be stored in a database and modified
@@ -26,17 +77,79 @@ 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
## Documentation
Here is a short example to illustrate interactive widgets:
A complete documentation for Owl can be found here:
```javascript
class ClickCounter extends owl.Component {
inlineTemplate = `
- [Main documentation page](doc/readme.md).
The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Component](doc/reference/component.md)
- [Hooks](doc/reference/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.24.1.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.js)
- [owl-0.24.1.min.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.min.js)
Some npm scripts are available:
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all (owl) tests |
## Quick Overview
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**Environment:** the root component is special: it is created with an environment,
which should contain a [`QWeb` instance](doc/reference/qweb.md). The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
@@ -45,42 +158,111 @@ class ClickCounter extends owl.Component {
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Note that the `t-on-click` handler can even be replaced by an inline statement:
```xml
<button t-on-click="state.value++">
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
const qweb = new owl.QWeb();
const counter = new ClickCounter({ qweb });
counter.mount(document.body);
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([{ id: 1, name: "Coffee", quantity: 0 }, { id: 2, name: "Tea", quantity: 0 }]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the section on [event handling](doc/reference/component.md#event-handling)
for more details on how events work.
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.9.0.js](https://github.com/odoo/owl/releases/download/v0.9.0/owl-0.9.0.js)
- [owl-0.9.0.min.js](https://github.com/odoo/owl/releases/download/v0.9.0/owl-0.9.0.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)) |
## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
## License
OWL is [GPL licensed](./LICENSE).
OWL is [LGPL licensed](./LICENSE).
-39
View File
@@ -1,39 +0,0 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<button class="flash" t-on-click="doSomething">Click</button>
```
with the following CSS:
```css
.flash {
transition: background 0.5s;
}
.flash:active {
background-color: #41454a;
transition: background 0s;
}
```
will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button.
## CSS Transitions (single element)
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us (see [QWeb documentation](doc/qweb.md#t-transition-directive)).
+34
View File
@@ -0,0 +1,34 @@
# 🦉 Rendering Pipeline 🦉
We explain here how Owl is designed, from the perspective of its rendering
pipeline.
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
+18
View File
@@ -0,0 +1,18 @@
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
+179 -29
View File
@@ -3,30 +3,61 @@
OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, some
In this page, we try to highlight some of these differences. Obviously, a lot of
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)
- [Hooks](#hooks)
### 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 | 18kb |
| Vue + VueX | 30kb |
| Vue + VueX + Vue Router | 39kb |
| React + ReactDOM + Redux | 40kb |
| jQuery | 30kb |
### Tooling/Build step
Note that those comparisons are not entirely fair, because we do not compare
the same exact set of features. For example, VueX and Vue Router support more
advanced use cases.
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
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).
Also, functions or class based components are more than just syntax. Functions
comes with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
It certainly looks like the world of UI frameworks is moving toward composition,
for many very good reasons. Owl is still good at composition (for example,
Owl supports slots, which is the primary mechanism to make generic reusable
components). But it can also use inheritance (and this is very important since
templates can also be inherited with `xpaths` transformations).
## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons,
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
@@ -36,14 +67,24 @@ be used by simply adding a script tag to a page.
<script src="owl.min.js" />
```
In comparison, React encourages using JSX,
which necessitate a build step, and most Vue applications uses single file
components, which also necessitate a build step.
In comparison, React encourages using JSX, which necessitate a build step, and
most Vue applications uses single file components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it
also brings a lot of benefits. And React/Vue have both a large ecosystem.
### Templating
Note that since Owl is not dependant on any external tool nor libraries, it is
very easy to integrate into any build toolchain. Also, since we cannot rely on
additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](reference/tags.md#xml-tag) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
## Templating
OWL uses its own QWeb engine, which compiles templates on the
frontend, as they are needed. This is extremely convenient for our use case, in
@@ -65,7 +106,8 @@ into javascript functions. Note that Vue has a separate build which includes the
template compiler.
In contrast, most React applications do not use a templating language, but write
some JSX code, which is precompiled into plain JavaScript by a build step.
some JSX code, which is precompiled into plain JavaScript by a build step. This
example is done with the (kind of outdated) React class system:
```jsx
class Clock extends React.Component {
@@ -84,22 +126,36 @@ 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
By comparison, here is the equivalent Owl component, written with the
[`xml`](reference/tags.md#xml-tag) tag helper:
```js
class Clock extends Component {
static template = xml`
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
`;
}
```
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
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() {
@@ -113,12 +169,14 @@ This may be dangerous (to stop the rendering waiting for the network), but it is
extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
more convoluted.
more convoluted. For example, in Vue, you need to use a dynamic import keyword
that needs to be transpiled at build time in order for the component to be loaded
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
### 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.
replaced with a new state (via the `setState` method). Then, the DOM is patched.
This is simple, efficient, and a little bit awkward to write.
Vue is a little bit different: it replace magically the properties in the state
@@ -126,10 +184,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 +248,97 @@ 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, with the `useStore` hook (see the [store documentation](reference/store.md#connecting-a-component)).
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
}
};
const state = {
counter: { value: 0 }
};
const store = new owl.Store({ state, actions });
class Counter extends Component {
static template = xml`
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>`;
counter = useStore(state => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
## Hooks
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
the React world. They solve a lot of seemingly unconnected problems: attach
reusable behavior to a component, in a composable way, extract stateful logic
from a component or reuse stateful logic between component, without changing your
component hierarchy.
Here is an example of the React `useState` hook:
```js
import React, { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
```
Because of the way React designed the hooks API, they only work for functional
components. But in that case, they really are powerful. Every major React library
is in the process of redesigning their API with hooks (for example,
[Redux](https://react-redux.js.org/next/api/hooks)).
Vue 2 does not have hooks, but the Vue project is working on its next version,
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
This work is based on the new ideas introduced by React hooks.
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](reference/hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
Here is what the `Counter` example above look like in Owl:
```js
import { Component, Owl } from "owl";
import { xml } from "owl/tags";
class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
count = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Since the Owl framework had hooks from early in its life, its main APIs
are designed to be interacted with hooks from the start. For example, the
`Context` and `Store` abstractions.
-316
View File
@@ -1,316 +0,0 @@
# 🦉 OWL Component 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Templates](#templates)
- [Reference](#reference)
- [Properties](#properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
## Overview
OWL components are the building blocks for user interface. They are designed to be:
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.
3. **asynchronous rendering:** the framework will transparently wait for each
subwidgets 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
and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (either defined inline or preloaded in QWeb).
The rendering is done by QWeb, which will generate a virtual dom representation
of the widget, then patch the DOM to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
## Example
Let us have a look at a simple component:
```javascript
class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 };
increment() {
this.state.value++;
}
}
```
```xml
<button t-name="click-counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
```
Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
Component class. It needs to define a template (`click-counter` here). Also,
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.
## Templates
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>
<span>some text</span>
<t t-widget="MyWidget" t-props="{info: 13}">
</div>
```
In this example, the template create 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.
## Reference
An Owl component is a small class which represent a widget 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.
### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
component is not mounted.
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`template`** (string): a string, which is the name of the QWeb template that will render
the component.
- **`inlineTemplate`** (string, optional): a string that represents a xml template. If set,
this will be loaded into QWeb and used instead of the `template` property.
- **`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.
- **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component.
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub widgets defined by a `t-ref` directive in the component's template.
### 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.
- **`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
useful to the underlying component system.
- **`render()`** (async): calling this method directly will cause a rerender. Note
that this should be very rare to have to do it manually, the Owl framework is
most of the time responsible for doing that at an appropriate moment.
Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the widget should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
- **`updateEnv(nextEnv)`**: update the environment of a component and all its
children. This forces a complete rerender. For example, this could be useful
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.
### 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)** | when component is render and in 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 |
Notes:
- hooks call order is precisely defined: `[willX]` hooks are called first on parent,
then on children, and `[Xed]` are called in the reverse order: first children,
then parent.
- no hook method should ever be called manually. They are supposed to be
called by the owl framework whenever it is required.
#### `constructor(parent, props)`
The constructor is not exactly a hook, it is the regular,
normal, constructor of the component. Since it is not a hook, you need to make
sure that `super` is called.
This is usually where you would set the initial state and the template of the
component.
```javascript
constructor(parent, props) {
super(parent, props);
this.state = {someValue: true};
this.template = 'mytemplate';
}
```
Note that with ESNext class fields, the constructor method does not need to be
implemented in most cases:
```javascript
class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 };
...
}
```
#### `willStart()`
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. Another use case is to load data from a server.
```javascript
async willStart() {
await owl.utils.loadJS("my-awesome-lib.js");
}
```
At this point, the component is not yet rendered. Note that a slow `willStart` method will slow down the rendering of the user
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.
#### `mounted()`
`mounted` is called each time a component is attached to the
DOM, after the initial rendering and possibly later if the component was unmounted
and remounted. At this point, the component is considered _active_. This is a good place to add some listeners, or to interact with the
DOM, if the component needs to perform some measure for example.
It is the opposite of `willUnmount`. If a component has been mounted, it will
always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First,
the parent, then all its children.
Note that the state is now observed. It is however allowed (but not encouraged)
to modify the state in the `mounted` hook. Doing so will cause a rerender,
which will not be perceptible by the user, but will slightly slow down the
component.
#### `willUpdateProps(nextProps)`
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).
```javascript
willUpdateProps(nextProps) {
return this.loadData({id: nextProps.id});
}
```
This hook is not called during the first render (but willStart is called
and performs a similar job).
#### `willPatch()`
The willPatch hook is called just before the DOM patching process starts.
It is not called on the initial render. This is useful to read some
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state object is not allowed here. This method is called just
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`).
The return value of this method will be given as the first argument of the
corresponding `patched` call.
#### `patched(snapshot)`
This hook is called whenever a component did actually update its DOM (most
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`).
The `snapshot` parameter is the result of the previous `willPatch` call.
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 patched. So, we need to be particularly
careful at avoiding endless cycles.
#### `willUnmount()`
willUnmount is a hook that is called each time just before a component is unmounted from
the DOM. This is a good place to remove some listeners, for example.
```javascript
mounted() {
this.env.bus.on('someevent', this, this.doSomething);
}
willUnmount() {
this.env.bus.off('someevent', this, this.doSomething);
}
```
This is the opposite method of `mounted`. The `willUnmount` method will be
called in reverse order: first the children, then the parents.
-54
View File
@@ -1,54 +0,0 @@
# 🦉 Observer 🦉
Owl need to be able to react to state changes. For example, whenever the state
of a component is changed, we need to rerender it. To help with that, we have
an Observer class. Its job is to observe some object state, and react to any
change. To do that, it recursively replace all keys of the observed state by
getters and setters.
For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
observer.observe(obj);
const obj = { a: { b: 1 } };
obj.a.b = 2;
```
## Technical Limitations
Since the observer uses getters and setters, it is actually unable to react to
changes in two situations:
- adding a key to an object:
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // will do nothing
```
In that case, we need a way to tell the observer that something happened.
This can be done by using the `set` method:
```javascript
observer.set(obj, "b", 2);
```
- 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" });
```
+12 -13
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.
@@ -55,29 +55,28 @@ To build an application (or a sub-part of an application), we need two things:
- an environment: it is the global context in which we are working. It needs to
contain a QWeb instance (preloaded with templates), and anything else that we
need. In practice, it could context some user session information, some
need. In practice, it could be used to contain 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`:
```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
constructor() {
super(...arguments);
this.template = "clickcounter";
this.state = { value: 0 };
}
static template = "clickcounter";
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -88,9 +87,9 @@ class ClickCounter extends owl.Component {
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
qweb: new owl.QWeb({ templates })
};
const counter = new ClickCounter(env);
const target = document.getElementById("main");
-361
View File
@@ -1,361 +0,0 @@
# 🦉 QWeb 🦉
## Content
- [Overview](#overview)
- [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)
- [`t-att` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
- [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)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate html. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the html.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
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))
## QWeb Specification
### Static html nodes
Normal, regular html nodes are rendered into themselves:
```xml
<div>hello</div> <!–– rendered as itself ––>
```
### `t-esc` directive
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
```xml
<p><t t-esc="value"/></p>
```
rendered with the value `value` set to `42` in the rendering context yields:
```html
<p>42</p>
```
### `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>
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
```html
<p><span>foo</span></p>
```
### `t-set` directive
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
1. a `t-value` attribute containing an expression, and the result of its
evaluation will be set:
```xml
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
```
will print `3`. Note that the evaluation is done at rendering time, not at
compilte time.
2. if there is no `t-value` attribute, the nodes body is saved and its value is
set as the variables value:
```xml
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
```
will generate `&lt;li&gt;ok&lt;/li&gt;` (the content is escaped as we used the `t-esc` directive)
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### `t-if` directive
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
```xml
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
```
The element is rendered if the condition (evaluated with the current rendering
context) is true:
```xml
<div>
<p>ok</p>
</div>
```
but if the condition is false it is removed from the result:
```xml
<div>
</div>
```
The conditional rendering applies to the bearer of the directive, which does not
have to be `<t>`:
```xml
<div>
<p t-if="condition">ok</p>
</div>
```
will give the same results as the previous example.
Extra conditional branching directives `t-elif` and `t-else` are also available:
```xml
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
```
### 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>
```
### `t-att` directive (dynamic attributes)
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
```xml
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
## JS/OWL Specific Extensions
### `t-on` directive
### Component: `t-widget`, `t-props`
### `t-ref` directive
### `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 creation:
- the css classes `name-enter` and `name-enter-active` will be added before the
node is added to 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 .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
```
Note: you will find more information on animations in the [animation](doc/animations.md)
documentation.
### Debugging (`t-debug` and `t-log`)
The javascript QWeb implementation provides two useful debugging directives:
`t-debug` adds a debugger statement during template rendering:
```xml
<t t-if="a_test">
<t t-debug="">
</t>
````
will stop execution if the browser dev tools are open.
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
```xml
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
```
will print 42 to the console
### 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.
+74 -7
View File
@@ -1,15 +1,82 @@
# 🦉 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
Context
QWeb
Store
useState
core
EventBus
Observer
hooks
onWillStart
onMounted
onWillUpdateProps
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
misc
AsyncRoot
router
Link
RouteComponent
Router
tags
xml
utils
debounce
escape
loadJS
loadFile
shallowEqual
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
- [Component](component.md)
- [QWeb](qweb.md)
- [Store](store.md)
- [Observer](observer.md)
- [Virtual DOM](vdom.md)
- [Animations](reference/animations.md)
- [Component](reference/component.md)
- [Context](reference/context.md)
- [Event Bus](reference/event_bus.md)
- [Hooks](reference/hooks.md)
- [Misc](reference/misc.md)
- [Observer](reference/observer.md)
- [QWeb](reference/qweb.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Learning Resources
- [Quick Start](quick_start.md)
## Miscellaneous
- [Quick Start](quick_start.md)
- [Animations](animations.md)
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
## Architecture
This section explains in more detail the inner workings of Owl. It is more
useful for people working on Owl code.
- [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md)
+95
View File
@@ -0,0 +1,95 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<a class="btn flash" t-on-click="doSomething">Click</a>
```
with the following CSS:
```css
btn {
background-color: gray;
}
.flash {
transition: background 0.5s;
}
.flash:active {
background-color: #41454a;
transition: background 0s;
}
```
will produce a nice flash effect whenever the user clicks (or activates with the
keyboard) the button.
## CSS Transitions
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us. It works on html elements and
on components, by adding and removing some css classes.
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
sequence of events 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:
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).
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
# 🦉 Context 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [`Context`](#context)
- [`useContext`](#usecontext)
## Overview
The `Context` object provides a way to share data between an arbitrary number
of components. Usually, data is passed from a parent to its children component,
but when we have to deal with some mostly global information, this can be
annoying, since each component will need to pass the information to each children,
even though some or most of them will not use the information.
With a `Context` object, each component can subscribe (with the `useContext` hook)
to its state, and will be updated whenever the context state is updated.
## Example
Assume that we have an application with various components which needs to render
differently depending on the size of the device. Here is how we could proceed
to make sure that the information is properly shared. First, let us create a
context, and add it to the environment:
```js
const deviceContext = new Context({ isMobile: true });
const env = {
qweb: new QWeb({ templates: TEMPLATES }),
deviceContext
};
```
If we want to make it completely responsive, we need to update its value whenever
the size of the screen is updated:
```js
const isMobile = () => window.innerWidth <= 768;
window.addEventListener(
"resize",
owl.utils.debounce(() => {
const state = deviceContext.state;
if (state.isMobile !== isMobile()) {
state.isMobile = !state.isMobile;
}
}, 15)
);
```
Then, each component that want can subscribe and render differently depending on the
fact that we are in a mobile or desktop mode.
```js
class SomeComponent extends Component {
static template = xml`
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
some more sopthisticated user interface
</t>
`;
device = useContext(this.env.deviceContext);
}
```
## Reference
### `Context`
A `Context` object should be created with a state object:
```js
const someContext = new Context({ some: "key" });
```
Its state is now available in the `state` key:
```js
someContext.state.some = "other key";
```
This is the way some global code (such as the responsive code above) should
read and update the context state. However, components should not ever read the
context state directly from the context, they should instead use the `useContext`
hook to properly register themselves to state changes.
Note that the `Context` hook is different from the React version. For example,
there is no concept of provider/consumer. So, the `Context` feature does not
by itself allow the use of a different context state depending on the component
place in the component tree. However, this functionality can be obtained, if
necessary, with the use of sub environment.
### `useContext`
The `useContext` hook is the normal way for a component to register themselve
to context state changes. The `useContext` method returns the context state:
```js
device = useContext(this.env.deviceContext);
```
It is a simple observed state (with an owl `Observer`), which contains the shared
information.
+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.core.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`.
+425
View File
@@ -0,0 +1,425 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [Example: Mouse Position](#example-mouse-position)
- [Example: Autofocus](#example-autofocus)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
- [`onWillStart`](#onwillstart)
- [`onWillUpdateProps`](#onwillupdateprops)
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Making customized hooks](#making-customized-hooks)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks work beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Example: autofocus
Hooks can be combined to create the desired effect. For example, the following
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
to create an easy way to focus an input whenever it appears in the DOM:
```js
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
constructor(...args) {
super(...args);
useAutofocus("myinput");
}
}
```
## Reference
### One rule
There is only one rule: every hook for a component has to be called in the
constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
In a hook, the `Component.current` static property is the reference to the
component instance that is currently being created. Hooks need to be called in
the constructor to ensure that this reference is properly set.
### `useState`
The `useState` hook is certainly the most important hook for Owl components:
this is what allows a component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
class Counter extends owl.Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
It is important to remember that `useState` only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
### `onMounted`
`onMounted` is not a user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not a user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
### `onWillStart`
`onWillStart` is an asynchronous hook. This means that the function registered
in the hook will be run just before the component is first rendered and can return a
promise, to express the fact that it is an asynchronous operation.
Note that if there are more than one `onWillStart` registered callback, then they
will all be run in parallel.
It can be used to load some initial data. For example, the following hook will
automatically load some data from the server, and return an object that will
be ready whenever the component is rendered:
```js
function useLoader() {
const component = Component.current;
const record = useState({});
onWillStart(async () => {
const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId));
});
return record;
}
```
Note that this example does not update the record value whenever props are
updated. For that situation, we need to use the `onWillUpdateProps` hook.
### `onWillUpdateProps`
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
designed to be run whenever the component props are updated. This could be
useful to perform some asynchronous task such as fetching updated data.
```js
function useLoader() {
const component = Component.current;
const record = useState({});
async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id));
}
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
return record;
}
```
Note that if there are more than one `onWillUpdateProps` registered callback,
then they will all be run in parallel.
### `useContext`
See [`useContext`](context.md#usecontext) for reference documentation.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references need to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only the component and its children
can access it:
```js
class FormComponent extends Component {
constructor(...args) {
super(...args);
const model = makeModel();
useSubEnv({ model });
}
}
```
The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
See the [store documentation](store.md) for more information.
### `useDispatch`
The `useDispatch` hook is the way for components to get a reference to the store
`dispatch` function. See the [store documentation](store.md) for more information.
### `useGetters`
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together.
But, like every good things in life, hooks should be used with moderation. They are
not the solution to every problem.
- they may be overkill: if your component needs to perform some action specific
to itself (so, the specific code does not need to be shared), there is nothing
wrong with a simple class method:
```js
// maybe overkill
class A extends Component {
constructor(...args) {
super(...args);
useMySpecificHook();
}
}
// ok
class B extends Component {
constructor(...args) {
super(...args);
this.performSpecificTask();
}
}
```
Note that the second solution is easier to extend in sub components.
- they may be harder to test: if a customized hook injects some external side
effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this:
```js
const router = new Router(...);
function useRouter() {
return router;
}
```
As you can see, this does not _hook_ into the internal of the component. It
simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the
environment.
```js
function useRouter() {
return Component.current.env.router;
}
```
This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+23
View File
@@ -0,0 +1,23 @@
# 🦉 Miscellaneous 🦉
## `AsyncRoot`
When this component is used, 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 />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
be a dom node or a component.
+52
View File
@@ -0,0 +1,52 @@
# 🦉 Observer 🦉
Owl needs to be able to react to state changes. For example, whenever the state
of a component is changed, Owl needs to rerender it. To help with that, there is
an Observer class. Its job is to observe the state of an object (or array), and
to react to any change. The observer is implemented with the native `Proxy`
object. Note that this means that it will not work on older browsers.
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
is the way most Owl applications will create observers. For the majority of
use cases, there is no need to directly instantiate an observer.
## Example
For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2;
```
This example shows that an observer can observe nested properties.
## Reference
**observe** An observer can observe multiple values with the `observe` method.
This method takes an object or an array as its argument and will return a proxy
(which is mapped to the initial object/array). With this proxy, the observer
can detect whenever any internal value is changed.
**Registering a callback** Whenever an observer sees a state change, it will
call its `notifyCB` method. No additional information is given to the callback.
**deepRevNumber** Each observed value has an internal revision number, which
is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.deepRevNumber(obj.a); // 1
obj.a.b = 2;
observer.deepRevNumber(obj.a); // 2
```
The `deepRevNumber` can also return 0, which indicates that the value is not
observed.
+612
View File
@@ -0,0 +1,612 @@
# 🦉 QWeb 🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message"/>
</li>
</ul>
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
## Directives
We present here a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`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
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White Spaces
White spaces in a template are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### 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 with a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that apply on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context (typically, the component):
```xml
<p t-if="user.birthday === today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | replaced with |
| ----- | ------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves:
```xml
<div>hello</div> <!–– rendered as itself ––>
```
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
```xml
<p><t t-esc="value"/></p>
```
rendered with the value `value` set to `42` in the rendering context yields:
```html
<p>42</p>
```
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe.
```xml
<p><t t-raw="value"/></p>
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
```html
<p><span>foo</span></p>
```
Note that since the content of the expression is not known beforehand, the `t-raw`
directive has to parse the html (and convert it to a virtual dom structure) for
each rendering. So, it will be much slower than a regular template. It is
therefore advised to limit the use of `t-raw` whenever possible.
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
1. a `t-value` attribute containing an expression, and the result of its
evaluation will be set:
```xml
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
```
will print `3`. Note that the evaluation is done at rendering time, not at
compilte time.
2. if there is no `t-value` attribute, the nodes body is saved and its value is
set as the variables value:
```xml
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
```
will generate `&lt;li&gt;ok&lt;/li&gt;` (the content is escaped as we used the `t-esc` directive)
The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
```xml
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
```
The element is rendered if the condition (evaluated with the current rendering
context) is true:
```xml
<div>
<p>ok</p>
</div>
```
but if the condition is false it is removed from the result:
```xml
<div>
</div>
```
The conditional rendering applies to the bearer of the directive, which does not
have to be `<t>`:
```xml
<div>
<p t-if="condition">ok</p>
</div>
```
will give the same results as the previous example.
Extra conditional branching directives `t-elif` and `t-else` are also available:
```xml
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
```
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
```xml
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
It is sometimes convenient to format an attribute with string interpolation. In
that case, the `t-attf-` directive can be used. It is useful when we need to mix
literal and dynamic elements, such as css classes.
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
If we need completely dynamic attribute names, then there is an additional
directive: `t-att`, which takes either an object (with keys mapping to their
values) or a pair `[key, value]`. For example:
```xml
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### 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 with the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratees size be
available
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i">
<t 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 -->
```
Owl QWeb is used as the template engine for components. Components are frequently
updated, and reuse as much of the previous DOM as possible. Loops offer a specific
problem for this usecase: how does the template engine know if two rows have
been swapped, or if the content of these rows was changed? To help Owl with that,
there is an additional directive: [`t-key`](component.md#t-key-directive).
```xml
<p t-foreach="state.things" t-as="thing" t-key="thing.id">
<t t-esc="thing.content"/>
</p>
```
If there is no `t-key` directive, Owl will use the index as a default key.
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
templates), using the `t-call` directive:
```xml
<div t-name="other-template">
<p><t t-value="var"/></p>
</div>
<div t-name="main-template">
<t t-set="var" t-value="owl"/>
<t t-call="other-template"/>
</div>
```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape.
Sometimes, one might want to pass information to the sub template. In that case,
the content of the body of the `t-call` directive is available as a special
magic variable `0`:
```xml
<t t-name="other-template">
This template was called with content:
<t t-raw="0"/>
</t>
<div t-name="main-template">
<t t-call="other-template">
<em>content</em>
</t>
</div>
```
will result in :
```xml
<div>
This template was called with content:
<em>content</em>
</div>
```
### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
`t-debug` adds a debugger statement during template rendering:
```xml
<t t-if="a_test">
<t t-debug="">
</t>
```
will stop execution if the browser dev tools are open.
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
```xml
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
```
will print 42 to the console.
+178
View File
@@ -0,0 +1,178 @@
# 🦉 Router 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [RouteComponent](#routecomponent)
- [Link](#link)
## Overview
It is often useful to organize an application around urls. If the application is
a single page application, then we need a way to manage those urls in the browser.
This is why there are many different routers for different frameworks. A generic
router can do the job just fine, but a specialized router for Owl can give a
better developer experience.
The Owl router support the following features:
- `history` or `hash` mode
- declarative routes
- route redirection
- navigation guards
- parameterized routes
- a `<Link/>` component
- a `<RouteComponent/>` component
Note that it is still in early stage of developments, and there are probably
still some issues.
## Example
To use the Owl router, there are some steps that needs to be done:
- declare some routes
- create a router
- add it to the environment
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
export const ROUTES = [
{ name: "LANDING", path: "/", component: Landing },
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
{ name: "SIGN_UP", path: "/signup", component: SignUp },
{ name: "SIGN_IN", path: "/signin", component: SignIn },
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
];
function makeEnvironment() {
...
const env = { qweb };
env.session = new Session(env);
env.router = new owl.router.Router(env, ROUTES);
await env.router.start();
return env;
}
```
Notice that the router needs to be started. This is an asynchronous operation
because it needs to apply the potential navigation guards on the current route
(which may or may not mean that the application is redirected to another route).
## Reference
### Route definition
A route need to be defined as an object with the following keys:
- `name` (optional): a (unique) string useful to identify the current route. If not
given, it will be assigned an automatic name,
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
It also can be `*`, to catch all remaining routes.
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
directive if the route is active
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
### `Router`
The `Router` constructor takes three arguments:
- `env`: a valid environment,
- a list of routes,
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
Example: `https://yourdomain.tld/my_custom_route`.\
For this mechanism to work, you need a way to configure your web server accordingly.
`hash` will manipulate the hash of the URL.\
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
```
Note that the route are defined in a list, and the order matters: the router
tries to find a match by going down the list.
The router needs to be added to the environment in the `router` sub key.
Once a router is created, it needs to be started. This is necessary to initialize
its current state to the current URL (and also, to potentially apply any
navigation guards and/or redirecting).
```js
await router.start();
```
Once started, the router will keep track of the current url and reflect its
value in two keys:
- `router.currentRoute`
- `router.currentParams`
The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url):
```js
router.navigate({ to: "USER", params: { id: 51 } });
```
### Navigation Guards
Navigation guards are very useful to be able to execute some business logic/
perform some actions or redirect to other routes whenever the application is
entering a new route. For example, the following guard checks if there is an
authenticated user, and if it is not the case, redirect to the sign in route.
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
```
A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object.
### `RouteComponent`
The `RouteComponent` component directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<RouteComponent />
</div>
```
### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any
content. It will compute the proper href from its props, and allow Owl to
properly navigate to a given url if clicked on it.
```xml
<Link to="'HOME'">Home</Link>
```
+326
View File
@@ -0,0 +1,326 @@
# 🦉 Store 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
Managing the state in an application is not an easy task. In some cases, the
state of an application can be part of the component tree, in a natural way.
However, there are situations where some parts of the state need to be displayed
in various parts of the user interface, and then, it is not obvious which
component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
Note: Owl store is inspired by React Redux and VueX.
## Example
Here is what a simple store looks like:
```js
const actions = {
addTodo({ state }, message) {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false
});
}
};
const state = {
todos: [],
nextId: 1
};
const store = new owl.Store({ state, actions });
store.on("update", null, () => console.log(store.state));
// updating the state
store.dispatch("addTodo", "fix all bugs");
```
This example shows how a store can be defined and used. Note that in most cases,
actions will be dispatched by connected components.
## Reference
### `Store`
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
whenever its state is changed. Note that these events are triggered only after a
microtask tick, so only one event will be triggered for any number of state changes in a
call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
The `Store` class is quite small. It has two public methods:
- its constructor
- `dispatch`
The constructor takes a configuration object with four (optional) keys:
- the initial state
- the actions
- the getters
- the environment
```javascript
const config = {
state,
actions,
getters,
env
};
const store = new Store(config);
```
### Actions
Actions are used to coordinate state changes. It can be used for both synchronous
and asynchronous logic.
```js
const actions = {
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo;
} catch (e) {
state.loginState = "error";
}
}
};
```
The first argument to an action method is an object with four keys:
- `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store,
- `env`: the current environment. This is useful sometimes, in particular if
an action needs to apply some side effects (such as performing an rpc), and
the `rpc` method is located in the environment.
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partially corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
}
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
}
};
```
### Getters
Usually, data contained in the store will be stored in a normalized way. For
example,
```js
{
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
authors: [{id: 4, name: 'John'}]
}
```
However, the user interface will probably need some denormalized data like
```js
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
```
This is what `getters` are for: they give a centralized way to process and
transform the data contained in the store.
```js
const getters = {
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => a.id === post.id);
return {
id,
author,
content: post.content
};
}
};
// somewhere else
const post = store.getters.getPost(id);
```
Getters take _at most_ one argument.
Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
Assume we have this store:
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
}
};
const state = {
counter: { value: 0 }
};
const store = new owl.Store({ state, actions });
```
A counter component can then select this value and dispatch an action like this:
```js
class Counter extends Component {
counter = useStore(state => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>
```
### `useStore`
The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and returns
an object or an array (which will be then observed),
- optionally, an object with a `store` key (if we want to override the default
store) and an equality function (if we want to specialize the comparison).
If the `useStore` callback selects a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check and will update the component every time this
check fails.
Also, it may not be obvious, but it is crucial to remember that the selector
function should return an object or an array. The reason is that it needs to be
observed, otherwise the component would not be able to react to changes.
### `useDispatch`
The `useDispatch` hook is useful when a component needs to be able to dispatch
actions. It takes an optional argument, which is a store. If not given, it will
use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class DoSomethingButton extends Component {
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
dispatch = useDispatch();
}
```
### `useGetters`
The `useGetters` hook is useful when a component needs to be able to use the
getters defined in a store. It takes an optional argument, which is a store. If
not given, it will use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class InfoButton extends Component {
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
getters = useGetters();
}
```
### Semantics
The `Store` class and the `useStore` hook try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children),
- they are updated only if they are in the DOM,
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components,
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and some other part of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately,
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `useStore`
call and a `Message` component could get the data of its own
message,
- since the `useStore` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+54
View File
@@ -0,0 +1,54 @@
# 🦉 Tags 🦉
## Content
- [Overview](#overview)
- [`xml` tag](#xml-tag)
## Overview
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
## XML tag
Without tags, creating a standalone component would look like this:
```js
import { Component } from 'owl'
const name = 'some-unique-name';
const template = `
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
QWeb.registerTemplate(name, template);
class MyComponent extends Component {
static template = name;
...
}
```
With tags, this process is slightly simplified. The name is uniquely generated,
and the template is automatically registered:
```js
const { Component } = owl;
const { xml } = owl.tags;
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
```
+148
View File
@@ -0,0 +1,148 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadFile`](#loadfile): loading a file (useful for templates)
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
- [`shallowEqual`](#shallowequal): shallow object comparison
## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb({ templates });
const app = new App({ qweb });
app.mount(document.body);
});
```
or alternatively:
```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 (by adding
a script tag in the document head). It returns a promise, so the caller can
properly reacts 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.
For example, it is useful for lazy loading external libraries:
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
## `loadFile`
`loadFile` is a helper function to fetch a file. It simply
performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example:
```js
async function makeEnv() {
const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb({ templates });
return { qweb };
}
```
Note that unlike `loadJS`, this function returns the content of the file as a
string. It does not add a `script` tag or any other side effect.
## `escape`
Sometimes, we need to display dynamic data (for example user-generated data) in
the user interface. If this is done by a `QWeb` template, it is not an issue:
```xml
<div><t t-esc="user.data"/></div>
```
The `QWeb` engine will create a `div` node and add the content of the `user.data`
string as a text node, so the web browser will not parse it as html. However,
it may be a problem if this is done with some javascript code like this:
```js
class BadComponent extends Component {
// some template with a ref to a div
// some code ...
mounted() {
this.divRef.el.innerHTML = this.state.value;
}
}
```
In this case, the content of the `div` will be parsed as html, which may inject
unwanted behaviour. To fix this, the `escape` function will simply transform a
string into an escaped version of the same string, which will be properly displayed
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
escaped to the string: `"&lt;ok&gt;"`). So, the bad example above can be fixed
with the following change:
```js
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
```
## `debounce`
The `debounce` function is useful when we want to limit the number of times some
function/action is perfomed. For example, this may be useful to prevent issue
with people double clicking on a button.
It takes three arguments:
- `func` (function): this is the function that will be rate limited
- `wait` (number): this is the number of milliseconds that we want to use to
rate limit the function `func`
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
function will be triggered immediately (leading edge of the interval). If false,
the function will be triggered at the end (trailing edge).
It returns a function. For example:
```js
const debounce = owl.utils.debounce;
window.addEventListener("mousemove", debounce(doSomething, 100));
```
As this example shows, it is usualy useful for event handlers which are triggered
very quickly, such as `scroll` or `mousemove` events.
## `shallowEqual`
This function checks if two objects have the same values assigned to each keys:
```js
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
```
However, for performance reasons, it assumes that the two objects have the same
keys. If we are in a situation where this is not guaranteed, the following code
will work:
```js
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
```
-8
View File
@@ -1,8 +0,0 @@
# 🦉 Store 🦉
Managing the state in an application is not an easy task. Many different
architectures/designs/systems/... have been created. We propose here to use
the idea of a central store.
- Store
- connect
+101
View File
@@ -0,0 +1,101 @@
# 🦉 Tooling 🦉
## Content
- [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
## Overview
To help work with/improve/learn OWL, there are a few extras tools/settings.
- development mode: enable better error reporting for the developer
- a playground application: a space to experiment and learn Owl.
- a benchmarks application: allow comparison with a few common frameworks
The two applications are available in the `tools/` folder, and can be accessed
by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground
The playground is an important application designed to help learning and
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
It is an application similar to `jsFiddle`, but specialized for Owl: there are
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
code in an iframe.
## Benchmarks
Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks.
## Single File Component
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb.md).
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
```js
const { Component } = owl;
const { xml } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
static template = TEMPLATE;
static components = { Sidebar, Content };
// rest of component...
}
```
Note that the above example has an inline xml comment, just after the `xml` call.
This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string.
-3
View File
@@ -1,3 +0,0 @@
# 🦉 VDom 🦉
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>
-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();
@@ -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>
-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`);
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL Extras</title>
</head>
<body>
<h1>🦉 OWL Extra Stuff 🦉</h1>
<h2>Benchmarks</h2>
<p>The goal is to compare some operations on large lists</p>
<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-master">OWL Master</a></li>
<li><a href="benchmarks/vue">Vue</a></li>
<li><a href="benchmarks/react">React</a></li>
</ul>
<h2>Playground</h2>
<a href="playground">Playground</a>
</body>
</html>
-325
View File
@@ -1,325 +0,0 @@
import { SAMPLES } from "./samples.js";
let owlJS;
async function owlSourceCode() {
if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const MODES = {
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
};
const DEFAULT_XML = `<templates>
</templates>`;
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `import sys
import thread
import webbrowser
import time
import BaseHTTPServer, SimpleHTTPServer
def start_server():
httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 3600), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.serve_forever()
thread.start_new_thread(start_server,())
url = 'http://127.0.0.1:3600'
webbrowser.open_new(url)
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
`;
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor() {
super(...arguments);
this.template = "tabbed-editor";
this.state = {
currentTab: this.props.display.split("|")[0]
};
this.tabs = {
js: this.props.display.includes("js"),
xml: this.props.display.includes("xml"),
css: this.props.display.includes("css")
};
}
mounted() {
this.editor = ace.edit(this.refs.editor);
// remove this for xml/css (?)
this.editor.session.setOption("useWorker", false);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("13px");
this.editor.setTheme("ace/theme/monokai");
this.editor.session.setMode(MODES[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", () => {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
});
}
patched() {
if (this.editor) {
window.dispatchEvent(new Event("resize"));
this.editor.setValue(this.props[this.state.currentTab], -1);
}
}
willUnmount() {
this.editor.destroy();
delete this.editor;
}
setTab(tab) {
this.editor.setValue(this.props[tab], -1);
const mode = MODES[tab];
this.editor.session.setMode(mode);
const tabSize = tab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.state.currentTab = tab;
}
onMouseDown(ev) {
if (ev.target.tagName === "DIV") {
let y = ev.clientY;
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
});
}
}
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.template = "playground";
this.version = owl._version;
this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor };
this.state = {
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
};
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.refs.content.innerHTML = "";
});
return;
}
}
async runCode() {
this.state.displayWelcome = false;
// check templates
var qweb = new owl.QWeb();
var error = false;
const sanitizedXML = this.state.xml.replace(/<!--[\s\S]*?-->/g, "");
try {
qweb.loadTemplates(sanitizedXML);
} catch (e) {
error = e;
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${
this.state.js
}`;
script.innerHTML = content;
const errorHandler = e => this.displayError(e.message);
iframe.contentWindow.addEventListener("error", errorHandler);
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
}
}, 100);
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = this.state.css;
doc.head.appendChild(style);
};
this.refs.content.innerHTML = "";
this.refs.content.appendChild(iframe);
}
setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
this.state[ev.type] = ev.value;
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.delta;
}
async downloadCode() {
await owl.utils.loadJS("libs/FileSaver.min.js");
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const JS = `async function startApp() {
// Loading templates
let TEMPLATES;
try {
TEMPLATES = await owl.utils.loadTemplates('app.xml');
} catch(e) {
document.write(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
// Application code
${this.state.js.split('\n').map(l => l === '' ? '' : ' ' + l).join('\n')}
}
// wait for DOM ready before starting
owl.utils.whenReady(startApp);`;
zip.file("app.js", JS);
zip.file("app.css", this.state.css);
zip.file("app.py", APP_PY);
zip.file("app.xml", this.state.xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
zip.generateAsync({ type: "blob" }).then(function(content) {
saveAs(content, "app.zip");
});
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
document.title = `${document.title} (v${owl._version})`;
document.addEventListener("DOMContentLoaded", async function() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
const env = { qweb };
const app = new App(env);
app.mount(document.body);
});
File diff suppressed because it is too large Load Diff
-46
View File
@@ -1,46 +0,0 @@
<templates>
<div t-name="tabbed-editor" class="tabbed-editor" t-att-style="props.style">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<a t-if="tabs.js" class="tab flash" t-att-class="{active: state.currentTab==='js'}" t-on-click="setTab('js')">JS</a>
<a t-if="tabs.xml" class="tab flash" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a>
<a t-if="tabs.css" class="tab flash" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a>
</div>
<div class="code-editor" t-ref="'editor'"></div>
</div>
<div t-name="playground" class="playground">
<div class="left-bar" t-att-style="leftPaneStyle" t-att-class="{split: state.splitLayout}">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<t t-if="!state.splitLayout">
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/>
</t>
<t t-else="1">
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js', style:topEditorStyle}" t-on-updateCode="updateCode"/>
<div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'xml|css', resizeable: true}" t-on-updateCode="updateCode" t-on-updatePanelHeight="updatePanelHeight"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
</div>
<div t-if="state.error" class="error">
<t t-esc="state.error"/>
</div>
<div class="content" t-ref="'content'"/>
</div>
</div>
</templates>
-6
View File
@@ -1,6 +0,0 @@
# 🦉 Extra Stuff 🦉
To help work/improve/learn with OWL, we have here:
- a benchmarks application
- a playground application
-34
View File
@@ -1,34 +0,0 @@
import sys
import thread
import webbrowser
import time
import BaseHTTPServer
import SimpleHTTPServer
HOST = '127.0.0.1'
PORT = 8000
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, ())
url = 'http://{0}:{1}/extras'.format(HOST, PORT)
webbrowser.open_new(url)
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
+19 -15
View File
@@ -1,51 +1,52 @@
{
"name": "owl",
"version": "0.9.0",
"name": "owl-framework",
"version": "0.24.1",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"engines": {
"node": ">=10.15.3"
},
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
"build:js:es5": "tsc --target es5 --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",
"build:es5": "npm run build:js:es5 && 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",
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
"test": "jest",
"test:watch": "jest --watch",
"preextras": "npm run build",
"extras": "python extras/server.py"
"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\"",
"prettier": "prettier {src/**/*.ts,tests/**/*.ts,doc/**/*.md} --write"
},
"repository": {
"type": "git",
"url": "git+https://github.com/odoo/owl.git"
},
"author": "Odoo",
"license": "GPL-3.0-or-later",
"license": "LGPL",
"bugs": {
"url": "https://github.com/odoo/owl/issues"
},
"homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": {
"@types/jest": "^23.3.12",
"cpx": "^1.5.0",
"git-rev-sync": "^1.12.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1",
"snabbdom-to-html": "^5.1.1",
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2",
"typescript": "^3.6.4",
"uglify-es": "^3.3.9"
},
"dependencies": {
"jest-environment-jsdom": "^24.7.1",
"snabbdom": "^0.7.3"
},
"jest": {
"roots": [
"<rootDir>/src",
@@ -64,5 +65,8 @@
"json",
"node"
]
},
"prettier": {
"printWidth": 100
}
}
+46
View File
@@ -0,0 +1,46 @@
# 🦉 OWL Roadmap 🦉
- Current version: 0.24.1
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### October 2019
We plan to complete the following tasks:
- improve API for root widgets (issue #306),
- replace `t-keepalive`, and maybe `t-transition` by components (issue #295).
### November 2019
Once the previous tasks are done, release version 1.0alpha. This means that the
API should be stable. But it could change a little bit if we need it for our
work on Odoo.
### End of 2019
Release v1.0
- API should be stable,
- we will use semantic versioning,
- we will maintain a changelog and an upgrade guide.
### 1.x
- add chrome and firefox devtools,
- add support for single file components,
- fix every bugs,
- improve documentation,
- small backward compatible improvements.
### 2.x (2021? 2022?)
Maybe:
- reimplement vdom to use *block* system, like Vue 3,
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations.
+1 -1
View File
@@ -9,6 +9,6 @@ export default {
format: "iife",
name: "owl",
extend: true,
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';`
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
}
};
-436
View File
@@ -1,436 +0,0 @@
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb";
import { h, patch, VNode } from "./vdom";
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
export interface Env {
qweb: QWeb;
}
interface Meta<T extends Env, Props> {
readonly id: number;
vnode: VNode | null;
isStarted: boolean;
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;
}
//------------------------------------------------------------------------------
// Widget
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
readonly __owl__: Meta<Env, Props>;
template: string = "default";
inlineTemplate: string | null = null;
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
env: T;
state?: State;
props: Props;
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();
// 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,
isStarted: false,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {}
};
}
/**
* 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 children = this.__owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
this.__owl__.isMounted = true;
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
this.__owl__.isMounted = false;
const children = this.__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> {
if (this.__owl__.isDestroyed) {
return;
}
const shouldCallPatchHooks: boolean = !patchQueue;
if (shouldCallPatchHooks) {
patchQueue = [];
}
const renderVDom = this._render(force, patchQueue);
const renderId = this.__owl__.renderId;
const vnode = await renderVDom;
if (renderId === this.__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.
if (shouldCallPatchHooks) {
for (let i = 0; i < patchQueue!.length; i++) {
const c = patchQueue![i];
c.__owl__.willPatchVal = c.willPatch();
}
}
this._patch(vnode);
if (shouldCallPatchHooks) {
for (let i = patchQueue!.length - 1; i >= 0; i--) {
const c = patchQueue![i];
c.patched(c.__owl__.willPatchVal);
}
}
}
}
destroy() {
if (!this.__owl__.isDestroyed) {
const el = this.el;
this._destroy(this.__owl__.parent);
if (el) {
el.remove();
}
}
}
_destroy(parent) {
const isMounted = this.__owl__.isMounted;
if (isMounted) {
this.willUnmount();
this.__owl__.isMounted = false;
}
const children = Object.values(this.__owl__.children);
for (let child of children) {
child._destroy(this);
}
if (parent) {
let id = this.__owl__.id;
delete parent.__owl__.children[id];
this.__owl__.parent = null;
}
this.clear();
this.__owl__.isDestroyed = true;
delete this.__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> {
if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (this.__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) {
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(false, patchQueue);
}
}
_patch(vnode) {
this.__owl__.renderPromise = null;
if (this.__owl__.vnode) {
this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
} else {
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
}
}
_prepare(): Promise<VNode> {
this.__owl__.renderProps = this.props;
this.__owl__.renderPromise = this._prepareAndRender();
return this.__owl__.renderPromise;
}
async _prepareAndRender(): Promise<VNode> {
await this.willStart();
if (this.__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
this.__owl__.isStarted = true;
if (this.inlineTemplate) {
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
}
this.__owl__.render = this.env.qweb.render.bind(
this.env.qweb,
this.inlineTemplate || this.template
);
this._observeState();
return this._render();
}
async _render(
force: boolean = false,
patchQueue: any[] = []
): Promise<VNode> {
if (this.__owl__.isMounted) {
patchQueue.push(this);
}
this.__owl__.renderId++;
const promises: Promise<void>[] = [];
if (this.__owl__.observer) {
this.__owl__.observer.allowMutations = false;
}
let vnode = this.__owl__.render!(this, {
promises,
handlers: this.__owl__.boundHandlers,
forceUpdate: force,
patchQueue
});
if (this.__owl__.observer) {
this.__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 = this.__owl__.id;
this.__owl__.renderProps = this.props;
this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
return this.__owl__.renderPromise;
}
/**
* Only called by qweb t-widget directive
*/
_mount(vnode: VNode, elm: HTMLElement): VNode {
this.__owl__.vnode = patch(elm, vnode);
if (
this.__owl__.parent &&
this.__owl__.parent.__owl__.isMounted &&
!this.__owl__.isMounted
) {
this._callMounted();
}
return this.__owl__.vnode;
}
__mount() {
if (!this.__owl__.isMounted) {
this.__owl__.isMounted = true;
this.mounted();
}
}
_observeState() {
if (this.state) {
this.__owl__.observer = new Observer();
this.__owl__.observer.observe(this.state);
this.__owl__.observer.notifyCB = this.render.bind(this);
}
}
}
+664
View File
@@ -0,0 +1,664 @@
import { Observer } from "../core/observer";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler } from "./scheduler";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
/**
* 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;
pvnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any> | null;
children: { [key: number]: Component<T, any> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
currentFiber: Fiber | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
renderFn: CompiledTemplate;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
willStartCB: Function | null;
willUpdatePropsCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>;
static template?: string | null = null;
static _template?: string | null = null;
static current: Component<any, any> | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
env: T;
props: Props;
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component.current = this;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, this.props);
}
let id: number = nextId++;
let p: Component<T, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
} else {
this.env = parent;
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);
}
});
}
const qweb = this.env.qweb;
this.__owl__ = {
id: id,
vnode: null,
pvnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
currentFiber: null,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)),
classObj: null,
refs: null
};
}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the component is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
* fast as possible.
*
* Note: this method should not be called manually.
*/
async willStart() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*/
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*
* It needs to be implemented by a component that is designed to handle the
* error properly.
*/
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 Promise.resolve();
}
if (__owl__.vnode && !renderBeforeRemount) {
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
return;
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
return new Promise((resolve, reject) => {
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
});
}
/**
* 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 && !__owl__.currentFiber) ||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
) {
return;
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, force);
this.__render(fiber);
return new Promise((resolve, reject) => {
scheduler.addFiber(fiber.root, err => {
if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
});
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
* level destroyed component, for performance reasons).
*
* The job of this method is mostly to call willUnmount hooks, and to perform
* all necessary internal cleanup.
*
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp.__callMounted();
}
}
__owl__.isMounted = true;
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
} catch (e) {
console.error(e); // TODO : add a test
}
}
__callWillUnmount() {
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp.__callWillUnmount();
}
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(
nextProps: Props,
parentFiber: Fiber,
scope: any,
vars: any,
previousSibling?: Fiber | null
): Promise<void> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
}
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
return;
}
this.props = nextProps;
this.__render(fiber);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
__owl__.currentFiber = null;
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
}
return this.__prepareAndRender(fiber);
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
} else {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
}
return p._template;
}
async __prepareAndRender(fiber: Fiber) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
this.__render(fiber);
}
}
__render(fiber: Fiber) {
const __owl__ = this.__owl__;
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
} catch (e) {
vnode = __owl__.vnode || h("div");
fiber.handleError(e);
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// we apply 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);
}
fiber.root.counter--;
fiber.isRendered = true;
}
/**
* Only called by qweb t-component directive
*/
__mount(fiber: Fiber, elm: HTMLElement): VNode {
if (fiber !== this.__owl__.currentFiber) {
fiber = this.__owl__.currentFiber!; // TODO: check if we can remove fiber arg
}
const vnode = fiber.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);
__owl__.currentFiber = null;
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
}
+497
View File
@@ -0,0 +1,497 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// 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 handlerValue = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerValue, 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);
}
}
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (ctx.lastNodeKey || ctx.currentKey) {
const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
}
const templateId = `templateId${componentID}`;
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
if (classAttr) {
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = owner['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.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;`
);
let shouldProxy = !ctx.parentNode;
if (keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (shouldProxy) {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
let registerCode = "";
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addIf(`w${componentID}`);
// need to update component
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars}, sibling)${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
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(keepAliveCode);
}
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addElse();
// new component
let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
return true;
}
});
+223
View File
@@ -0,0 +1,223 @@
import { VNode } from "../vdom/index";
import { Component } from "./component";
/**
* Owl Fiber Class
*
* Fibers are small abstractions designed to contain all the internal state
* associated with a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*
* Fibers capture all that necessary information, which is critical to owl
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
* states and in general determine the state of the rendering.
*/
export class Fiber {
// The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCancelled means that the rendering corresponding to this fiber and its
// children is cancelled. No extra work should be done.
isCancelled: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
// fibers corresponding to a new component do not need to do that. So, the
// shouldPatch hook is the boolean that we check whenever we need to apply
// a patch.
shouldPatch: boolean = true;
// isRendered is the last state of a fiber. If true, this means that it has
// been rendered and is inert (so, it should not be taken into account when
// counting the number of active fibers).
isRendered: boolean = false;
// the counter number is a critical information. It is only necessary for a
// root fiber. For that fiber, this number counts the number of active sub
// fibers. When that number reaches 0, the fiber can be applied by the
// scheduler.
counter: number = 0;
scope: any;
vars: any;
props: any;
component: Component<any, any>;
vnode: VNode | null = null;
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
parent: Fiber | null = null;
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) {
this.force = force;
this.scope = scope;
this.vars = vars;
this.props = props;
this.component = component;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) {
this._remapFiber(oldFiber);
}
this.root.counter++;
component.__owl__.currentFiber = this;
}
/**
* In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component
* tree. In that case, it needs to cancel the previous rendering and
* remap itself as a part of the current parent rendering.
*/
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
if (oldFiber === oldFiber.root) {
oldFiber.root.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
this.parent = oldFiber.parent;
this.root = this.parent.root;
this.sibling = oldFiber.sibling;
if (this.parent.child === oldFiber) {
this.parent.child = this;
} else {
let current = this.parent.child!;
while (true) {
if (current.sibling === oldFiber) {
current.sibling = this;
break;
}
current = current.sibling!;
}
}
}
}
/**
* This function has been taken from
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
*/
_walk(doWork: (f: Fiber) => Fiber | null) {
let root = this;
let current: Fiber = this;
while (true) {
const child = doWork(current);
if (child) {
current = child;
continue;
}
if (current === root) {
return;
}
while (!current.sibling) {
if (!current.parent || current.parent === root) {
return;
}
current = current.parent;
}
current = current.sibling;
}
}
/**
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
*/
patchComponents() {
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) {
patchQueue.push(f);
}
return f.child;
};
this._walk(doWork);
let component: Component<any, any> = this.component;
const patchLen = patchQueue.length;
try {
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
} catch (e) {
console.error(e);
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
try {
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
console.error(e);
}
}
/**
* Cancel a fiber and all its children.
*/
cancel() {
this._walk(f => {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
return f.child;
});
}
/**
* 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.
*/
handleError(error: Error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError!(error);
});
} else {
this.root.error = error;
root.destroy();
}
}
}
+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 (!(propName in props)) {
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;
}
+67
View File
@@ -0,0 +1,67 @@
import { Fiber } from "./fiber";
/**
* Owl Scheduler Class
*
* The scheduler is the part of Owl that will effectively apply a rendering
* whenever a fiber is ready.
*
* Briefly, it can be used to register root fibers. Whenever there is an
* active root fiber, it will poll continuously each animation frame (so, about
* once every 16ms) and whenever a root fiber is ready, it will apply it.
*/
interface Task {
fiber: Fiber;
callback: (err?: Error) => void;
}
export class Scheduler {
tasks: Task[] = [];
isRunning: boolean = false;
requestAnimationFrame: typeof window.requestAnimationFrame;
constructor(requestAnimationFrame) {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
this.scheduleTasks();
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
*/
flush() {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
if (task.fiber.isCancelled) {
return false;
}
if (task.fiber.counter === 0) {
task.callback(task.fiber.error);
return false;
}
return true;
});
this.tasks = tasks.concat(this.tasks);
}
scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => {
this.flush();
if (this.tasks.length > 0) {
this.scheduleTasks();
} else {
this.isRunning = false;
}
});
}
}
+111
View File
@@ -0,0 +1,111 @@
import { Component, scheduler } from "./component/component";
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { onWillUnmount } from "./hooks";
/**
* The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component,
* but when we have to deal with some mostly global information, this can be
* annoying, since each component will need to pass the information to each
* children, even though some or most of them will not use the information.
*
* With a `Context` object, each component can subscribe (with the `useContext`
* hook) to its state, and will be updated whenever the context state is updated.
*/
export class Context extends EventBus {
state: any;
observer: Observer;
rev: number = 1;
// mapping from component id to last observed context id
mapping: { [componentId: number]: number } = {};
constructor(state: Object = {}) {
super();
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(state);
}
/**
* 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.
*
* A potential cheap way to improve this situation is to keep track of the
* depth of a component in the component tree. A root component has a depth of
* 1, then its children of 2 and so on... Then, we can update all components
* with the same depth in parallel.
*/
async __notifyComponents() {
const rev = ++this.rev;
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) {
const render = sub.callback.call(sub.owner, rev);
scheduler.flush();
await render;
}
}
}
}
/**
* The`useContext` hook is the normal way for a component to register themselve
* to context state changes. The `useContext` method returns the context state
*/
export function useContext(ctx: Context): any {
const component: Component<any, any> = Component.current!;
return useContextWithCB(ctx, component, component.render.bind(component));
}
export function useContextWithCB(ctx: Context, component: Component<any, any>, method): any {
const __owl__ = component.__owl__;
const id = __owl__.id;
const mapping = ctx.mapping;
if (id in mapping) {
return ctx.state;
}
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function() {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0;
const renderFn = __owl__.renderFn;
__owl__.renderFn = function(comp, params) {
mapping[id] = ctx.rev;
return renderFn(comp, params);
};
ctx.on("update", component, async contextRev => {
if (mapping[id] < contextRev) {
mapping[id] = contextRev;
await method();
}
});
onWillUnmount(() => {
ctx.off("update", component);
delete mapping[id];
});
return ctx.state;
}
+5 -3
View File
@@ -3,7 +3,8 @@
* - emit events
* - add/remove listeners.
*
* This is a useful pattern of communication in some cases.
* This is a useful pattern of communication in many cases. For OWL, each
* components and stores are event buses.
*/
//------------------------------------------------------------------------------
@@ -28,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
@@ -63,7 +64,8 @@ export class EventBus {
*/
trigger(eventType: string, ...args: any[]) {
const subs = this.subscriptions[eventType] || [];
for (let sub of subs) {
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
sub.callback.call(sub.owner, ...args);
}
}
+109
View File
@@ -0,0 +1,109 @@
/**
* Owl Observer
*
* This code contains the logic that allows Owl to observe and react to state
* changes.
*
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
* - Objects and arrays are observed by replacing them with a Proxy
* - each object/array metadata are tracked in a weakmap, and keep a revision
* number
*
* Note that this code is loosely inspired by Vue.
*/
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
return value;
}
let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy;
}
revNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0;
}
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) {
var self = this;
const proxy = new Proxy(value, {
get(target, k) {
const targetValue = target[k];
return self.observe(targetValue, value);
},
set(target, key: string, newVal): boolean {
const value = target[key];
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
}
return true;
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
}
return true;
}
});
const metadata = {
value,
proxy,
rev: this.rev,
deepRev: this.rev,
parent
};
this.weakMap.set(value, metadata);
this.weakMap.set(metadata.proxy, metadata);
return metadata;
}
_updateRevNumber(target: any) {
this.rev++;
let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.deepRev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
+128
View File
@@ -0,0 +1,128 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
// -----------------------------------------------------------------------------
// useState
// -----------------------------------------------------------------------------
/**
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component.current!;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
// -----------------------------------------------------------------------------
// Life cycle hooks
// -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) {
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
current.call(component);
cb.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
} else {
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
cb.call(component);
current.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
}
}
function makeAsyncHook(method: string) {
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function(...args) {
return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
};
} else {
component.__owl__[method] = cb;
}
};
}
export const onMounted = makeLifecycleHook("mountedCB", true);
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
export const onWillPatch = makeLifecycleHook("willPatchCB");
export const onPatched = makeLifecycleHook("patchedCB", true);
export const onWillStart = makeAsyncHook("willStartCB");
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
}
export function useRef(name: string): Ref {
const __owl__ = Component.current!.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
+51 -5
View File
@@ -1,8 +1,54 @@
export { Component } from "./component";
export { EventBus } from "./event_bus";
export { Observer } from "./observer";
export { QWeb } from "./qweb";
export { connect, Store } from "./store";
/**
* This file is the main file packaged by rollup (see rollup.config.js). From
* this file, we export all public owl elements.
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index";
import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router";
export { Component } from "./component/component";
export { QWeb };
export const Context = _context.Context;
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const misc = { AsyncRoot };
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
useGetters: _store.useGetters,
useStore: _store.useStore
});
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+19
View File
@@ -0,0 +1,19 @@
import { Component } from "../component/component";
import { xml } from "../tags";
/**
* AsyncRoot
*
* Owl is by default asynchronous, and the user interface will wait for all its
* subcomponents to be rendered before updating the DOM. This is most of the
* time what we want, but in some cases, it makes sense to "detach" a component
* from this coordination. This is the goal of the AsyncRoot component.
*/
export class AsyncRoot extends Component<any, any> {
static template = xml`<t t-slot="default"/>`;
async __updateProps(nextProps, parentFiber) {
this.render(parentFiber.force);
}
}
-142
View File
@@ -1,142 +0,0 @@
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto);
for (let method of methodsToPatch) {
const initialMethod = ArrayProto[method];
ModifiedArrayProto[method] = function(...args) {
if (!this.__observer__.allowMutations) {
throw new Error(`Array cannot be changed here")`);
}
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 elem of inserted) {
this.__observer__.observe(elem, this);
}
}
return initialMethod.call(this, ...args);
};
}
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
notifyCB() {}
notifyChange() {
this.rev++;
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._addProp(target, key, value);
target.__owl__.rev++;
this.notifyChange();
}
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
const keys = Object.keys(obj);
obj.__owl__ = { rev: 1, deepRev: 1, parent };
Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key of keys) {
this._addProp(obj, key, obj[key]);
}
}
_observeArr(arr: Array<any>, parent?: any) {
(<any>arr).__owl__ = { rev: 1, deepRev: 1, parent };
Object.defineProperty(arr, "__owl__", { enumerable: false });
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
(<any>arr).__proto__.__observer__ = this;
for (let i = 0; i < arr.length; 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) {
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);
}
}
-1198
View File
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
import { CompilationContext } from "./compilation_context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
/**
* Owl QWeb Directives
*
* This file contains the implementation of most standard QWeb directives:
* - t-esc
* - t-raw
* - t-set/t-value
* - t-if/t-elif/t-else
* - t-call
* - t-foreach/t-as
* - t-debug
* - t-log
*/
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
if (value === "0" && ctx.caller) {
qweb._compileNode(ctx.caller, ctx);
return;
}
if (value.xml instanceof NodeList) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
}
} else {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
}
QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
}
});
QWeb.addDirective({
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }): boolean {
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
}
});
//------------------------------------------------------------------------------
// t-set
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
}
return true;
}
});
//------------------------------------------------------------------------------
// t-if, t-elif, t-else
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "if",
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "elif",
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "else",
priority: 40,
atNodeEncounter({ ctx }): boolean {
ctx.addLine(`else {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
//------------------------------------------------------------------------------
// t-call
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!;
const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new CompilationContext();
tempCtx.nextID = ctx.rootContext.nextID;
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
ctx.rootContext.nextID = tempCtx.nextID;
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
ctx.indent();
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
}
// todo: handle XML variables...
}
}
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
ctx.dedent();
ctx.addLine("}");
}
return true;
}
});
//------------------------------------------------------------------------------
// t-foreach
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "foreach",
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name + "_index", loopVar);
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key");
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
return true;
}
});
//------------------------------------------------------------------------------
// t-debug
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
});
//------------------------------------------------------------------------------
// t-log
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
}
});
+183
View File
@@ -0,0 +1,183 @@
import { compileExpr, QWebVar, QWebExprVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class CompilationContext {
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: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldDefineSibling: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
lastNodeKey: string = ""; // temp variable to communicate to previous caller
templates: { [key: string]: boolean } = {};
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineSibling) {
this.code.unshift(" let sibling = null;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
}
withParent(node: number): CompilationContext {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
if (!this.parentNode && this.rootContext.shouldDefineResult) {
this.addLine(`result = vn${node};`);
}
return this.subContext("parentNode", node);
}
subContext(key: keyof CompilationContext, value: any): CompilationContext {
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): QWebExprVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
return compileExpr(expr, this.variables);
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
+264
View File
@@ -0,0 +1,264 @@
/**
* Owl QWeb Expression Parser
*
* Owl needs in various contexts to be able to understand the structure of a
* string representing a javascript expression. The usual goal is to be able
* to rewrite some variables. For example, if a template has
*
* ```xml
* <t t-if="computeSomething({val: state.val})">...</t>
* ```
*
* this needs to be translated in something like this:
*
* ```js
* if (context["computeSomething"]({val: context["state"].val})) { ... }
* ```
*
* This file contains the implementation of an extremely naive tokenizer/parser
* and evaluator for javascript expressions. The supported grammar is basically
* only expressive enough to understand the shape of objects, of arrays, and
* various operators.
*/
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
type TKind =
| "LEFT_BRACE"
| "RIGHT_BRACE"
| "LEFT_BRACKET"
| "RIGHT_BRACKET"
| "LEFT_PAREN"
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "SYMBOL"
| "OPERATOR"
| "COLON";
interface Token {
type: TKind;
value: string;
size?: number;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
"]": "RIGHT_BRACKET",
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
return false;
}
let i = 1;
let cur;
while (expr[i] && expr[i] !== start) {
cur = expr[i];
s += cur;
if (cur === "\\") {
i++;
cur = expr[i];
if (!cur) {
throw new Error("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new Error("Invalid expression");
}
s += start;
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
let i = 1;
while (expr[i] && expr[i].match(/[0-9]|\./)) {
s += expr[i];
i++;
}
return { type: "VALUE", value: s };
} else {
return false;
}
};
let tokenizeSymbol: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1;
while (expr[i] && expr[i].match(/\w/)) {
s += expr[i];
i++;
}
if (s in WORD_REPLACEMENT) {
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
}
return { type: "SYMBOL", value: s };
} else {
return false;
}
};
const tokenizeStatic: Tokenizer = function(expr) {
const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
}
return false;
};
const tokenizeOperator: Tokenizer = function(expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
}
}
return false;
};
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
* example: `tokenize("1 + b")` will return:
* ```js
* [
* {type: "VALUE", value: "1"},
* {type: "OPERATOR", value: "+"},
* {type: "SYMBOL", value: "b"}
* ]
* ```
*/
export function tokenize(expr: string): Token[] {
const result: Token[] = [];
let token: boolean | Token = true;
while (token) {
expr = expr.trim();
if (expr) {
for (let tokenizer of TOKENIZERS) {
token = tokenizer(expr);
if (token) {
result.push(token);
expr = expr.slice(token.size || token.value.length);
break;
}
}
} else {
token = false;
}
}
if (expr.length) {
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
}
return result;
}
//------------------------------------------------------------------------------
// Expression "evaluator"
//------------------------------------------------------------------------------
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
* proper lookups in the context.
*
* Usually, this kind of code would be very simple to do if we had an AST (so,
* if we had a javascript parser), since then, we would only need to find the
* variables and replace them. However, a parser is more complicated, and there
* are no standard builtin parser API.
*
* Since this method is applied to simple javasript expressions, and the work to
* be done is actually quite simple, we actually can get away with not using a
* parser, which helps with the code size.
*
* Here is the heuristic used by this method to determine if a token is a
* variable:
* - by default, all symbols are considered a variable
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
} else {
token.value = `context['${token.value}']`;
}
}
}
result += token.value;
}
return result;
}
+276
View File
@@ -0,0 +1,276 @@
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 "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
if (handlerName) {
if (!extraArgs) {
handler += `const fn = context['${handlerName}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
handler += `}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
}
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
whenTransitionEnd(elm, finalize);
});
};
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
whenTransitionEnd(elm, finalize);
});
};
function getTimeout(delays: Array<string>, durations: Array<string>): number {
/* istanbul ignore next */
while (delays.length < durations.length) {
delays = delays.concat(delays);
}
return Math.max.apply(
null,
durations.map((d, i) => {
return toMs(d) + toMs(delays[i]);
})
);
}
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
function toMs(s: string): number {
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
}
function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
if (!ctx.parentNode) {
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.nextID++}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
event = "change";
handler = `(ev) => {${expr} = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
handler = `(ev) => {${expr} = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
handler = `(ev) => {${expr} = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
//------------------------------------------------------------------------------
// t-key
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.lastNodeKey = `nodeKey${id}`;
}
});
+4
View File
@@ -0,0 +1,4 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
+769
View File
@@ -0,0 +1,769 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
/**
* Owl QWeb Engine
*
* In this file, you will find a QWeb engine/template compiler. It is the core
* of how Owl component works.
*
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
* DOM representation.
*
* We have here:
* - a CompilationContext class, which is an internal object that contains all
* compilation specific information, while a template is being compiled.
* - a QWeb class: this is the code of the QWeb compiler.
*
* Note that this file does not contain the implementation of the QWeb
* directives (see qweb_directives.ts and qweb_extensions.ts).
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type EvalContext = { [key: string]: any };
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
interface Template {
elem: Element;
fn: CompiledTemplate;
}
interface CompilationInfo {
node: Element;
qweb: QWeb;
ctx: CompilationContext;
fullName: string;
value: string;
}
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive {
name: string;
extraNames?: string[];
priority: number;
// if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void;
}
interface QWebConfig {
templates?: string;
translateFn?(text: string): string;
}
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)",
destroy: "()"
};
interface Utils {
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
const UTILS: Utils = {
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return expr;
},
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
}
};
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc;
}
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template };
static utils = UTILS;
static components = Object.create(null);
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
name: 1,
att: 1,
attf: 1,
translation: 1
};
static DIRECTIVES: Directive[] = [];
static TEMPLATES: { [name: string]: Template } = {};
static nextId: number = 1;
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
static slots = {};
static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
constructor(config: QWebConfig = {}) {
super();
this.templates = Object.create(QWeb.TEMPLATES);
if (config.templates) {
this.addTemplates(config.templates);
}
if (config.translateFn) {
this.translateFn = config.translateFn;
}
}
static addDirective(directive: Directive) {
if (directive.name in QWeb.DIRECTIVE_NAMES) {
throw new Error(`Directive "${directive.name} already registered`);
}
QWeb.DIRECTIVES.push(directive);
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
static registerComponent(name: string, Component: any) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.components[name] = Component;
}
/**
* Register globally a template. All QWeb instances will obtain their
* templates from their own template map, and then, from the global static
* TEMPLATES property.
*/
static registerTemplate(name: string, template: string) {
if (QWeb.TEMPLATES[name]) {
throw new Error(`Template '${name}' has already been registered`);
}
const qweb = new QWeb();
qweb.addTemplate(name, template);
QWeb.TEMPLATES[name] = qweb.templates[name];
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
}
this._addTemplate(name, <Element>doc.firstChild);
}
/**
* Load templates from a xml (as a string or xml document). This will look up
* for the first <templates> tag, and will consider each child of this as a
* template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr: string | Document) {
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
}
for (let elem of <any>templates.children) {
const name = elem.getAttribute("t-name");
this._addTemplate(name, elem);
}
}
_addTemplate(name: string, elem: Element) {
if (name in this.templates) {
throw new Error(`Template ${name} already defined`);
}
this._processTemplate(elem);
const template = {
elem,
fn: function(this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
}
};
this.templates[name] = template;
}
_processTemplate(elem: Element) {
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i];
let prevElem = node.previousElementSibling!;
let pattr = function(name) {
return prevElem.getAttribute(name);
};
let nattr = function(name) {
return +!!node.getAttribute(name);
};
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) {
throw new Error(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
);
}
if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
return a + b;
}) > 1
) {
throw new Error("Only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode.nodeValue.trim().length) {
throw new Error("text is not allowed between branching directives");
}
textNode.remove();
}
} else {
throw new Error(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
);
}
}
}
/**
* Render a template
*
* @param {string} name the template should already have been added
*/
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
const template = this.templates[name];
if (!template) {
throw new Error(`Template ${name} does not exist`);
}
return template.fn.call(this, context, extra);
}
/**
* Render a template to a html string.
*
* Note that this is more limited than the `render` method: it is not suitable
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
}
/**
* Force all widgets connected to this QWeb instance to rerender themselves.
*
* This method is mostly useful for external code that want to modify the
* application in some cases. For example, a router plugin.
*/
forceUpdate() {
this.isUpdating = true;
Promise.resolve().then(() => {
if (this.isUpdating) {
this.isUpdating = false;
this.trigger("update");
}
});
}
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
if (!parentContext) {
if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`);
} else {
if (!ctx.rootNode) {
throw new Error(`A template should have one root node (${ctx.templateName})`);
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: CompilationContext) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
text = this.translateFn(text);
}
}
if (ctx.parentNode) {
if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (node.nodeType === 8) {
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
}
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
const finalizers: typeof validDirectives = [];
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.createElement("t");
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child);
}
node.appendChild(tNode);
node.removeAttribute(attrName);
}
}
}
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
let withHandlers = false;
for (let i = 0; i < DIR_N; i++) {
let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
finalizers.push({ directive, value, fullName });
}
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value
});
if (isDone) {
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
return;
}
}
}
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
let nodeHooks = {};
let addNodeHook = function(hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
directive.atNodeCreation({
node,
qweb: this,
ctx,
fullName,
value,
nodeID,
addNodeHook
});
}
}
if (Object.keys(nodeHooks).length) {
ctx.addLine(`p${nodeID}.hook = {`);
for (let hook in nodeHooks) {
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
for (let handler of nodeHooks[hook]) {
ctx.addLine(` ${handler}`);
}
ctx.addLine(` },`);
}
ctx.addLine(`};`);
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS =
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
}
_compileGenericNode(
node: ChildNode,
ctx: CompilationContext,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
let value = attributes[i].textContent!;
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
value = this.translateFn(value);
}
// regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
const formattedExpr = ctx.interpolate(value);
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`var _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey = ctx.lastNodeKey || nodeID;
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
ctx.addElse();
ctx.addLine(`for (let key in _${id}) {`);
ctx.indent();
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
ctx.dedent();
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: CompilationContext) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./router";
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>
`;
href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) {
this.href = this.env.router.destToPath(nextProps);
}
get isActive() {
if (this.env.router.mode === "hash") {
return (<any>document.location).hash === this.href;
}
return (<any>document.location).pathname === this.href;
}
navigate(ev) {
// don't redirect with control keys
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
// don't redirect on right click
if (ev.button !== undefined && ev.button !== 0) {
return;
}
// don't redirect if `target="_blank"`
if (ev.currentTarget && ev.currentTarget.getAttribute) {
const target = ev.currentTarget.getAttribute("target");
if (/\b_blank\b/i.test(target)) {
return;
}
}
ev.preventDefault();
this.env.router.navigate(this.props);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Component } from "../component/component";
import { xml } from "../tags";
export class RouteComponent extends Component<any, {}> {
static template = xml`
<t>
<t
t-if="routeComponent"
t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t>
`;
get routeComponent(): any {
return this.env.router.currentRoute && this.env.router.currentRoute.component;
}
}
+275
View File
@@ -0,0 +1,275 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { shallowEqual } from "../utils";
type NavigationGuard = (info: {
env: Env;
to: Route | null;
from: Route | null;
}) => boolean | Destination;
export interface Route {
name: string;
path: string;
component?: any;
redirect?: Destination;
params: string[];
beforeRouteEnter?: NavigationGuard;
}
export type RouteParams = { [key: string]: string | number };
export interface RouterEnv extends Env {
router: Router;
}
export interface Destination {
path?: string;
to?: string;
params?: RouteParams;
}
interface PositiveMatchResult {
type: "match";
route: Route;
params: RouteParams;
}
interface NegativeMatchResult {
type: "nomatch";
}
interface CancelledMatch {
type: "cancelled";
}
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
interface Options {
mode: Router["mode"];
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
currentRoute: Route | null = null;
currentParams: RouteParams | null = null;
mode: "history" | "hash";
routes: { [id: string]: Route };
routeIds: string[];
env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
this.routes = {};
this.routeIds = [];
let nextId = 1;
for (let partialRoute of routes) {
if (!partialRoute.name) {
partialRoute.name = "__route__" + nextId++;
}
if (partialRoute.component) {
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
}
if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
(this as any)._listener = ev => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener);
}
const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") {
this.currentRoute = result.route;
this.currentParams = result.params;
const currentPath = this.routeToPath(result.route, result.params);
if (currentPath !== this.currentPath()) {
this.setUrlFromPath(currentPath);
}
}
}
async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to);
return this._navigate(path);
}
async _navigate(path: string, ev?: any): Promise<boolean> {
const initialName = this.currentRouteName;
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath);
}
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
const didChange =
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
if (didChange) {
this.env.qweb.forceUpdate();
return true;
}
return false;
}
destToPath(dest: Destination): string {
this.validateDestination(dest);
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
}
get currentRouteName(): string | null {
return this.currentRoute && this.currentRoute.name;
}
//--------------------------------------------------------------------------
// Private helpers
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const separator = this.mode === "hash" ? location.pathname : "";
const url = location.origin + separator + path;
if (url !== window.location.href) {
window.history.pushState({}, path, url);
}
}
private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
}
}
private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
const prefix = this.mode === "hash" ? "#" : "";
return prefix + parts.join("/");
}
private currentPath(): string {
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
return result || "/";
}
private match(path: string): MatchResult {
for (let routeId of this.routeIds) {
let route = this.routes[routeId];
let params = this.getRouteParams(route, path);
if (params) {
return {
type: "match",
route: route,
params: params
};
}
}
return { type: "nomatch" };
}
private async matchAndApplyRules(path: string): Promise<MatchResult> {
const result = this.match(path);
if (result.type === "match") {
return this.applyRules(result);
}
return result;
}
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
const route = matchResult.route;
if (route.redirect) {
const path = this.destToPath(route.redirect);
return this.matchAndApplyRules(path);
}
if (route.beforeRouteEnter) {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route
});
if (result === false) {
return { type: "cancelled" };
} else if (result !== true) {
// we want to navigate to another destination
const path = this.destToPath(result);
return this.matchAndApplyRules(path);
}
}
return matchResult;
}
private getRouteParams(route: Route, path: string): RouteParams | false {
if (route.path === "*") {
return {};
}
if (path.startsWith("#")) {
path = path.slice(1);
}
const descrParts = route.path.split("/");
const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false;
}
const result = {};
for (let i = 0; i < l; i++) {
const descr = descrParts[i];
let target: string | number = targetParts[i];
const match = descr.match(paramRegexp);
if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
}
}
return result;
}
}
function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1].split(".")[0]);
}
} while (m);
return result;
}
+108 -184
View File
@@ -1,218 +1,142 @@
import { Component } from "./component";
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
/**
* Owl Store
*
* We have here:
* - a Store class
* - useStore hook
* - useDispatch hook
* - useGetters hook
*
* 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?: any;
env?: Env;
state?: any;
actions?: any;
mutations?: { [name: string]: any };
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
export class Store extends Context {
actions: any;
mutations: any;
_commitLevel: number = 0;
history: any[] = [];
debug: boolean;
env: any;
observer: Observer;
set: any;
getters: { [name: string]: (payload?) => any };
updateFunctions: { [key: number]: (() => boolean)[] };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.state = config.state || {};
constructor(config: StoreConfig) {
super(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);
if (this.debug) {
this.history.push({ state: this.state });
this.getters = {};
this.updateFunctions = [];
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);
}
}
this.set = this.observer.set.bind(this.observer);
}
dispatch(action, payload?: any): Promise<void> | void {
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
},
payload
);
if (result instanceof Promise) {
return new Promise((resolve, reject) => {
result.then(() => resolve());
result.catch(reject);
});
}
}
commit(type, payload?: 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
...payload
);
return result;
}
}
if (this._commitLevel === 1) {
this.observer.allowMutations = false;
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: payload
});
}
interface SelectorOptions {
store?: Store;
isEqual?: (a: any, b: any) => boolean;
}
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const store = options.store || (component.env.store as Store);
let result = selector(store.state, component.props);
const hashFn = store.observer.deepRevNumber.bind(store.observer);
let revNumber = hashFn(result) || result;
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) {
store.updateFunctions[component.__owl__.id] = [];
}
const updateFunctions = store.updateFunctions[component.__owl__.id];
updateFunctions.push(function(): boolean {
const oldResult = result;
result = selector(store!.state, component.props);
const newRevNumber = hashFn(result);
if (
(newRevNumber > 0 && revNumber !== newRevNumber) ||
(newRevNumber === 0 && !isEqual(oldResult, result))
) {
revNumber = newRevNumber;
return true;
}
this._commitLevel--;
return res;
}
return false;
});
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
updateFunctions.forEach(function(updateFn) {
shouldRender = updateFn() || shouldRender;
});
if (shouldRender) {
return component.render();
}
});
onWillUpdateProps(props => {
// FIXME: only do that if not keepalive + do it in destroy in that case
delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props);
});
return new Proxy(result, {
get(target, k) {
return result[k];
},
set(target, k, v) {
result[k] = v;
return true;
}
});
}
//------------------------------------------------------------------------------
// 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;
export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env.store as Store);
return store.dispatch.bind(store);
}
function deepRevNumber<T extends Object>(o: T): number {
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
return (<any>o).__owl__.deepRev;
}
return 0;
}
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(Comp) {
return 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);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
this.__owl__.ownProps = ownProps;
this.__owl__.currentStoreProps = storeProps;
this.__owl__.storeHash = hashFunction(
{
state: env.store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
}
mounted() {
this.env.store.on("update", this, () => {
const ownProps = this.__owl__.ownProps;
const storeProps = mapStateToProps(this.env.store.state, ownProps);
const options: any = {
currentStoreProps: this.__owl__.currentStoreProps
};
const storeHash = hashFunction(
{
state: this.env.store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== this.__owl__.storeHash) {
didChange = true;
this.__owl__.storeHash = storeHash;
}
if (didChange) {
this.__owl__.currentStoreProps = storeProps;
this._updateProps(ownProps, false);
}
});
super.mounted();
}
willUnmount() {
this.env.store.off("update", this);
super.willUnmount();
}
_updateProps(nextProps, forceUpdate) {
if (this.__owl__.ownProps !== nextProps) {
this.__owl__.currentStoreProps = mapStateToProps(
this.env.store.state,
nextProps
);
}
this.__owl__.ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
this.__owl__.currentStoreProps
);
return super._updateProps(mergedProps, forceUpdate);
}
};
};
export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env.store as Store);
return store.getters;
}
+27
View File
@@ -0,0 +1,27 @@
import { QWeb } from "./qweb/index";
/**
* Owl Tags
*
* We have here a (very) small collection of tag functions:
*
* - xml
*
* The plan is to add a few other tags such as css, globalcss.
*/
/**
* XML tag helper for defining templates. With this, one can simply define
* an inline template with just the template xml:
* ```js
* class A extends Component {
* static template = xml`<div>some template</div>`;
* }
* ```
*/
export function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
return name;
}
+73 -127
View File
@@ -1,129 +1,23 @@
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
}
export type HashFn = (args: any[]) => string;
export function memoize<R, T extends (...args: any[]) => R>(
f: T,
hash?: HashFn
): T {
if (!hash) {
hash = args => args.map(a => String(a)).join(",");
}
let cache: { [key: string]: R } = {};
function memoizedFunction(...args: any[]) {
let hashValue = hash!(args);
if (!(hashValue in cache)) {
cache[hashValue] = f(...args);
}
return cache[hashValue];
}
return memoizedFunction as T;
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
* Owl Utils
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
* We have here a small collection of utility functions:
*
* - whenReady
* - loadJS
* - loadFile
* - escape
* - debounce
*/
export function debounce(
func: Function,
wait: number,
immediate?: boolean
): Function {
let timeout;
return function(this: any) {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
if (document.readyState !== "loading") {
resolve();
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export function patch(C: any, patchName: string, patch: any) {
const proto = C.prototype;
if (!proto.__patches) {
proto.__patches = {
origMethods: {},
patches: {},
current: []
};
}
if (proto.__patches.patches[patchName]) {
throw new Error(`Patch [${patchName}] already exists`);
}
proto.__patches.patches[patchName] = patch;
applyPatch(proto, patch);
proto.__patches.current.push(patchName);
function applyPatch(proto, patch) {
Object.keys(patch).forEach(function(methodName) {
const method = patch[methodName];
if (typeof method === "function") {
const original = proto[methodName];
if (!(methodName in proto.__patches.origMethods)) {
proto.__patches.origMethods[methodName] = original;
}
proto[methodName] = function(...args) {
this._super = original;
return method.call(this, ...args);
};
}
});
}
}
export function unpatch(C: any, patchName: string) {
const proto = C.prototype;
const patchInfo = proto.__patches;
delete proto.__patches;
// reset to original
for (let k in patchInfo.origMethods) {
proto[k] = patchInfo.origMethods[k];
}
// apply other patches
for (let name of patchInfo.current) {
if (name !== patchName) {
patch(C, name, patchInfo.patches[name]);
}
}
}
export async function loadTemplates(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
let templates = await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
}).then(fn || function() {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -149,10 +43,62 @@ export function loadJS(url: string): Promise<void> {
return promise;
}
export function whenReady(fn) {
if (document.readyState === "complete") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn, false);
export async function loadFile(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return await result.text();
}
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout;
return function(this: any) {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export function shallowEqual(p1, p2): boolean {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
+29
View File
@@ -0,0 +1,29 @@
import { VNode, h } from "./vdom";
const parser = new DOMParser();
export function htmlToVDOM(html: string): VNode[] {
const doc = parser.parseFromString(html, "text/html");
const result: VNode[] = [];
for (let child of doc.body.childNodes) {
result.push(htmlToVNode(child));
}
return result;
}
function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) {
return { text: node.textContent! } as VNode;
}
const attrs = {};
for (let attr of node.attributes) {
attrs[attr.name] = attr.textContent;
}
const children: VNode[] = [];
if (node.hasChildNodes) {
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
}
return h((node as Element).tagName, { attrs }, children);
}
+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;
+83 -388
View File
@@ -1,12 +1,47 @@
/**
* Owl VDOM
*
* This file contains an implementation of a virtual DOM, which is a system that
* can generate in-memory representations of a DOM tree, compare them, and
* eventually change a concrete DOM tree to match its representation, in an
* hopefully efficient way.
*
* Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
* needs (see https://github.com/snabbdom/snabbdom).
*
* The main exported values are:
* - interface VNode
* - h function (a helper function to generate a vnode)
* - patch function (to apply a vnode to an actual DOM node)
*/
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
declare global {
interface Element {
setAttribute(name: string, value: string | number | boolean): void;
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void;
setAttributeNS(
namespaceURI: string,
qualifiedName: string,
value: string | number | boolean
): void;
}
}
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
//------------------------------------------------------------------------------
@@ -21,7 +56,7 @@ export interface VNode {
key: Key | undefined;
}
interface VNodeData {
export interface VNodeData {
props?: Props;
attrs?: Attrs;
on?: On;
@@ -39,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 };
}
//------------------------------------------------------------------------------
@@ -96,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,
@@ -125,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) {
@@ -145,6 +160,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any,
iLen: number,
data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.init))) {
@@ -160,25 +176,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
}
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf("#");
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 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; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
? api.createElementNS(i, sel)
: api.createElement(sel));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0; i < children.length; ++i) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
@@ -216,13 +220,15 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function invokeDestroyHook(vnode: VNode) {
let i: any,
iLen: number,
j: number,
jLen: number,
data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0; i < cbs.destroy.length; ++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; j < vnode.children.length; ++j) {
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
@@ -240,6 +246,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
): void {
for (; startIdx <= endIdx; ++startIdx) {
let i: any,
iLen: number,
listeners: number,
rm: () => void,
ch = vnodes[startIdx];
@@ -248,12 +255,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; i < cbs.remove.length; ++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();
@@ -315,11 +318,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 {
@@ -346,11 +345,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];
}
@@ -359,31 +354,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
) {
let i: any, hook: any;
if (
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
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))) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm as Node);
@@ -391,19 +371,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; i < cbs.update.length; ++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(
@@ -415,23 +390,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);
}
@@ -441,9 +406,9 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
let i: number, iLen: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
@@ -463,12 +428,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
insertedVnodeQueue[i]
);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
for (i = 0, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
return vnode;
};
}
@@ -489,21 +454,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 {
@@ -522,11 +479,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);
}
@@ -554,22 +507,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,
@@ -581,11 +518,7 @@ const htmlDomApi = {
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment
setTextContent
} as DOMAPI;
//------------------------------------------------------------------------------
@@ -623,21 +556,19 @@ type VNodeChildElement = VNode | string | number | undefined | null;
type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
if (sel === "dummy") {
// we do not need to add the namespace on dummy elements, they come from a
// subcomponent, which will handle the namespace itself
return;
}
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
for (let i = 0, iLen = children.length; i < iLen; ++i) {
const child = children[i];
let childData = child.data;
if (childData !== undefined) {
addNS(
childData,
(children[i] as VNode).children as VNodes,
children[i].sel
);
addNS(childData, (child as VNode).children as VNodes, child.sel);
}
}
}
@@ -651,7 +582,8 @@ export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {},
children: any,
text: any,
i: number;
i: number,
iLen: number;
if (c !== undefined) {
data = b;
if (array(c)) {
@@ -673,247 +605,10 @@ export function h(sel: any, b?: any, c?: any): VNode {
}
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
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 (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
}
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
type Props = Record<string, any>;
function updateProps(oldVnode: VNode, vnode: VNode): void {
var key: string,
cur: any,
old: any,
elm = vnode.elm,
oldProps = (oldVnode.data as VNodeData).props,
props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in oldProps) {
if (!props[key]) {
delete (elm as any)[key];
}
}
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule = {
create: updateProps,
update: updateProps
} as Module;
//------------------------------------------------------------------------------
// module/module.ts
//------------------------------------------------------------------------------
interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
type On = {
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
} & {
[event: string]: EventListener;
};
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call handler with arguments
if (typeof handler[0] === "function") {
// special case for single argument for performance
if (handler.length === 2) {
handler[0].call(vnode, handler[1], event, vnode);
} else {
var args = handler.slice(1);
args.push(event);
args.push(vnode);
handler[0].apply(vnode, args);
}
} else {
// call multiple handlers
for (var i = 0; i < handler.length; 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]);
+133
View File
@@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
`;
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'jupiler');
},
remove: (vn, rm) => {
utils.transitionRemove(vn, 'jupiler', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
},
remove: (vn, rm) => {
utils.transitionRemove(vn, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
-155
View File
@@ -1,155 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`composition sub widgets with some state rendered in a loop 1`] = `
"function anonymous(context,extra
) {
let owner = context;
context = Object.create(context);
var h = this.utils.h;
var 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 _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
context.number_first = i === 0;
context.number_last = i === _3.length - 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 = {};
let isNew7 = !w7;
if (w7 && w7.__owl__.renderPromise) {
if (w7.__owl__.isStarted) {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
} else {
isNew7 = true
if (props7 === w7.__owl__.renderProps) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false
}
}
}
if (!def6) {
if (w7) {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
} else {
w7 = new context.widgets['ChildWidget'](owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7._prepare();
}
}
if (isNew7) {
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}; w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let vnode;if (!w7.__owl__.vnode){vnode=w7.__owl__.pvnode} else { vnode=h(w7.__owl__.vnode.sel, {key: key8});vnode.elm=w7.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w7.el,a.elm);a.elm=w7.el;w7.__mount();},remove(){w7.destroy()}, destroy() {w7.destroy()}}}c1[_5_index]=vnode;});
}
extra.promises.push(def6);
}
return vn1;
}"
`;
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var 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};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: key5});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var 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 = {};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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>"`;
exports[`connecting a component to store deep and shallow connecting a component 1`] = `"<div><span>Kasteel</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 2`] = `"<div><span>Kasteel</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`;
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
+379
View File
@@ -0,0 +1,379 @@
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
patchNextFrame,
renderToDOM,
unpatchNextFrame
} from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - qweb: a new QWeb instance
// - env: a WEnv, necessary to create new components
// - cssEl: a stylesheet injected into the dom
let fixture: HTMLElement;
let qweb: QWeb;
let env: Env;
let cssEl: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
qweb = new QWeb();
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("animations", () => {
beforeEach(() => {
cssEl = document.createElement("style");
let css = `
.chimay-enter-active, .chimay-leave-active {
transition-property: opacity;
transition-duration: 0.1s;
}
.chimay-enter, .chimay-leave-to {
opacity: 0;
}
`;
cssEl.textContent = css.trim();
document.head.appendChild(cssEl);
});
afterEach(() => {
document.head.removeChild(cssEl);
unpatchNextFrame();
});
test("t-transition, on a simple node (insert)", async () => {
expect.assertions(5);
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(node.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
node.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(node.className).toBe("");
});
test("t-transition with no delay/duration", async () => {
expect.assertions(4);
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
cb();
expect(node.className).toBe("");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
await def;
});
test("t-transition on a conditional node", async () => {
expect.assertions(7);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = useState({ hide: false });
}
const widget = new TestWidget(env);
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
// remove span from the DOM
def = makeDeferred();
widget.state.hide = true;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-ref", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget(env);
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with component", async () => {
expect.assertions(5);
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
}
const widget = new Parent(env);
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
);
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
});
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-component="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
state = useState({ display: true });
}
const widget = new Parent(env);
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
);
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
// remove span from the DOM
def = makeDeferred();
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<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" data-owl-key="__4__">blue</span></div>'
);
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe("<div></div>");
});
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 {
state = useState({ 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 {
static components = { Child };
state = useState({ 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,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
+447
View File
@@ -0,0 +1,447 @@
import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb";
import { xml } from "../../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
let dev: boolean = false;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
dev = QWeb.dev;
QWeb.dev = true;
});
afterEach(() => {
fixture.remove();
QWeb.dev = dev;
});
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("props validation", () => {
test("validation is only done in dev mode", async () => {
class TestWidget extends Widget {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
QWeb.dev = true;
expect(() => {
new TestWidget(env);
}).toThrow();
QWeb.dev = false;
expect(() => {
new TestWidget(env);
}).not.toThrow();
});
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'message' (component 'TestWidget')");
});
test("validate simple types", async () => {
const Tests = [
{ type: Number, ok: 1, ko: "1" },
{ type: Boolean, ok: true, ko: "1" },
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
];
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: test.type };
};
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in component");
}
});
test("validate simple types, alternate form", async () => {
const Tests = [
{ type: Number, ok: 1, ko: "1" },
{ type: Boolean, ok: true, ko: "1" },
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
];
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: test.type } };
};
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in component");
}
});
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] };
};
expect(() => {
new TestWidget(env, { p: "string" });
new TestWidget(env, { p: true });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in component");
});
test("can validate an optional props", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } };
};
expect(() => {
new TestWidget(env, { p: "hey" });
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 template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } };
};
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: [1] });
}).toThrow();
expect(() => {
new TestWidget(env, { p: ["string", 1] });
}).toThrow();
});
test("can validate an array with multiple sub element types", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } };
};
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
new TestWidget(env, { p: [false, true, "string"] });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: [true, 1] });
}).toThrow();
});
test("can validate an object with simple shape", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
};
expect(() => {
new TestWidget(env, { p: { id: 1, url: "url" } });
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: "1", url: "url" } });
}).toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1 } });
}).toThrow();
});
test("can validate recursively complicated prop def", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = {
p: {
type: Object,
shape: {
id: Number,
url: [Boolean, { type: Array, element: Number }]
}
}
};
};
expect(() => {
new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12] } });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow();
});
test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Child message="1"/>
</div>
<div t-name="Child"><t t-esc="props.message"/></div>
</templates>`);
class Child extends Widget {
static props = ["message"];
}
class App extends Widget {
static components = { Child };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component
// creation, and one at update time.
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("props: list of strings with optional props", async () => {
class TestWidget extends Widget {
static props = ["message", "someProp?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
}).toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("props: can be defined with a boolean", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, {});
}).toThrow();
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: extra props cause an error, part 2", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: optional prop do not cause an error", async () => {
class TestWidget extends Widget {
static props = ["message?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("optional prop do not cause an error if value is undefined", async () => {
class TestWidget extends Widget {
static props = { message: { type: String, optional: true } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: undefined });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: null });
}).toThrow();
});
test("missing required boolean prop causes an error", async () => {
class TestWidget extends Widget {
static props = ["p"];
static template = xml`<span><t t-if="props.p">hey</t></span>`;
}
class App extends Widget {
static template = xml`<div><TestWidget/></div>`;
static components = { TestWidget };
}
const w = new App(env, {});
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
});
test("props are validated whenever component is updated", async () => {
let error;
class TestWidget extends Component<any, any> {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
async __updateProps() {
try {
await Component.prototype.__updateProps.apply(this, arguments);
} catch(e) {
error = e;
}
}
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
});
test("default values are applied before validating props at update", async () => {
class TestWidget extends Component<any, any> {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
});
describe("default props", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
static defaultProps = { p: 4 };
static template = xml`<div>hey</div>`;
}
const w = new TestWidget(env, {});
expect(w.props.p).toBe(4);
});
test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget {
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
class Parent extends Widget {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("can set default required boolean values", async () => {
class TestWidget extends Widget {
static props = ["p", "q"];
static defaultProps = { p: true, q: false };
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
}
class App extends Widget {
static template = xml`<div><TestWidget/></div>`;
static components = { TestWidget };
}
const w = new App(env, {});
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
});
});
+224
View File
@@ -0,0 +1,224 @@
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { Context, useContext } from "../src/context";
import { xml } from "../src/tags";
import { useState } from "../src/hooks";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("Context", () => {
test("very simple use, with initial value", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
});
test("useContext hook is reactive, for one component", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
test.contextObj.value = 321;
await nextTick();
expect(fixture.innerHTML).toBe("<div>321</div>");
});
test("two components can subscribe to same context", async () => {
const testContext = new Context({ value: 123 });
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("one components can subscribe twice to same context", async () => {
const testContext = new Context({ a: 1, b: 2 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></span>`;
contextObj1 = useContext(testContext);
contextObj2 = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
expect(steps).toEqual(["child"]);
testContext.state.a = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>32</span></div>");
expect(steps).toEqual(["child", "child"]);
});
test("parent and children subscribed to same context", async () => {
const testContext = new Context({ a: 123, b: 321 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
static components = { Child };
contextObj = useContext(testContext);
__render(fiber) {
steps.push("parent");
return super.__render(fiber);
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
expect(steps).toEqual(["parent", "child"]);
parent.contextObj.a = 124;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>124</span>321</div>");
// we only want one render from the child here, not two
expect(steps).toEqual(["parent", "child", "parent", "child"]);
});
test("destroyed component is inactive", async () => {
const testContext = new Context({ a: 123 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
__render(fiber) {
steps.push("parent");
return super.__render(fiber);
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(steps).toEqual(["parent", "child"]);
expect(testContext.subscriptions.update.length).toBe(1);
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["parent", "child", "parent"]);
// kind of whitebox...
// we make sure we do not have any pending subscriptions to the 'update'
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
let stateC;
class ComponentC extends Component<any, any> {
static template = xml`<span><t t-esc="context[props.key].n"/><t t-esc="state.x"/></span>`;
context = useContext(testContext);
state = useState({ x: "a" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component<any, any> {
static components = { ComponentC };
static template = xml`<p><ComponentC key="props.key"/></p>`;
willUpdateProps() {
return def;
}
}
class ComponentA extends Component<any, any> {
static components = { ComponentB };
static template = xml`<div><ComponentB key="context.key"/></div>`;
context = useContext(testContext);
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
testContext.state.key = "y";
testContext.state.y = { n: 2 };
delete testContext.state.x;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
stateC.x = "b";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>2b</span></p></div>");
});
});
@@ -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")');
});
});
+188
View File
@@ -0,0 +1,188 @@
/**
* 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";
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
interface MarkDownLink {
name: string;
link: string;
}
interface MarkDownSection {
name: string;
slug: string;
}
interface FileData {
name: string;
path: string[];
fullName: string
links: MarkDownLink[];
sections: MarkDownSection[];
}
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
export function addMardownData(fileData): void {
const sep = fileData.path.length > 0 ? '/' : '';
const fullName = fileData.path.join('/') + sep + fileData.name;
const content = fs.readFileSync(fullName, { 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);
}
/**
* Returns a list of FileData corresponding to all files that need to be
* validated.
*/
function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) {
const baseFiles: FileData[] = [
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" }
];
const rest = getFiles(["doc"]);
const result = baseFiles.concat(rest);
result.forEach(addMardownData);
return result;
}
const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => {
if (f.isDirectory()) {
return getFiles(path.concat(f.name));
}
const fullName = path.join('/') + (path.length > 0 ? '/' : '') + f.name;
return [
{
name: f.name,
path,
links: [],
sections: [],
fullName
}
];
});
return Array.prototype.concat(...files);
}
const LOCAL_FILES = ['LICENSE'];
export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) {
// no check on external links
return true;
}
// Step 1: extract path, name, hash
// path = ['doc', 'architecture]
// name = 'rendering.md'
// hash = 'blabla' (or '' if no hash)
const parts = link.link.split('#');
const hash = parts[1] || '';
let name;
let path;
if (parts[0]) {
let temp = parts[0].split('/');
name = temp[temp.length - 1];
temp.splice(-1);
path = current.path.slice();
for (let elem of temp) {
if (elem === '..') {
path.splice(-1);
} else if (elem !== '.') {
path.push(elem);
}
}
} else {
// there are no file name, so this is a relative link to the current file
name = current.name;
path = current.path;
}
// Step 2: build normalized link file name
const linkFullName = path.join('/') + (path.length > 0 ? '/' : '') + name;
// Step 3: check link name against white list of local files
if (LOCAL_FILES.includes(linkFullName)) {
return true;
}
// Step 4: check if there is a matching file
let target: FileData | undefined = files.find(f => f.fullName === linkFullName);
if (!target) {
return false;
}
// Step 5: if necessary, check if there is a corresponding link inside the target
// link name
if (hash) {
if (!target.sections.find(s => s.slug === hash)) {
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
}
//--------------------------------------------------------------------------
// Test
//--------------------------------------------------------------------------
test("All markdown links work", () => {
let linkNumber = 0;
let invalidLinkNumber = 0;
const data = getFiles();
for (let file of data) {
for (let link of file.links) {
linkNumber++;
if (!isLinkValid(link, file, data)) {
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
invalidLinkNumber++;
}
}
}
expect(invalidLinkNumber).toBe(0);
expect(linkNumber).toBeGreaterThan(10);
});
+107 -5
View File
@@ -1,12 +1,45 @@
import { Env } from "../src/component";
import { QWeb } from "../src/qweb";
import { Env, scheduler } from "../src/component/component";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { patch } from "../src/vdom";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
import "../src/component/directive";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
setTimeout(callback, 1);
return 1;
};
// Some static cleanup
let nextSlotId;
let slots;
let nextId;
let TEMPLATES;
beforeEach(() => {
nextSlotId = QWeb.nextSlotId;
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
});
afterEach(() => {
QWeb.nextSlotId = nextSlotId;
QWeb.slots = slots;
QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES;
});
// helpers
export function nextMicroTick(): Promise<void> {
return Promise.resolve();
}
export function nextTick(): Promise<void> {
return new Promise(resolve => setTimeout(resolve));
export async function nextTick(): Promise<void> {
return new Promise(function(resolve) {
setTimeout(() => scheduler.requestAnimationFrame(() => resolve()));
});
}
export function makeTestFixture() {
@@ -35,8 +68,77 @@ export function makeDeferred(): Deferred {
return <Deferred>def;
}
export function makeTestWEnv(): Env {
export function makeTestEnv(): Env {
return {
qweb: new QWeb()
};
}
export function trim(str: string): string {
return str.replace(/\s/g, "");
}
export function renderToDOM(
qweb: QWeb,
template: string,
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
// change.
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
if (vnode.sel === undefined) {
return document.createTextNode(vnode.text!);
}
const node = document.createElement(vnode.sel!);
const result = patch(node, vnode);
return result.elm as HTMLElement;
}
/**
* Render a template to an html string. The big difference with the
* renderToString method in QWeb is that we use the renderToDom method, which
* snapshots the resulting template function. Doing so gives us a large body
* of reference to evaluate changes in the generated QWeb code.
*
* Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb.
*/
export function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result;
}
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
// is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards.
let nextFrame = QWeb.utils.nextFrame;
export function patchNextFrame(f: Function) {
QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb));
};
}
export function unpatchNextFrame() {
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();
}
+493
View File
@@ -0,0 +1,493 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import {
useState,
onMounted,
onWillUnmount,
useRef,
onPatched,
onWillPatch,
onWillStart,
onWillUpdateProps,
useSubEnv
} from "../src/hooks";
import { xml } from "../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("hooks", () => {
test("can use a state hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3</div>");
});
test("can use onMounted, onWillUnmount", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("can use onMounted, onWillUnmount, part 2", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
expect(steps).toEqual(["mounted"]);
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order, part 2", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
class Parent extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("two different call to mounted/willunmount should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onMounted(() => {
steps.push("hook:mounted" + i);
});
onWillUnmount(() => {
steps.push("hook:willunmount" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual([
"hook:mounted1",
"hook:mounted2",
"hook:willunmount2",
"hook:willunmount1"
]);
});
test("useRef hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
test("useRef hook is null if ref is removed ", async () => {
expect.assertions(4);
class TestRef extends Component<any, any> {
static template = xml`<div><span t-if="state.flag" t-ref="span">owl</span></div>`;
spanRef = useRef("span");
state = useState({ flag: true });
willPatch() {
expect(this.spanRef.el).not.toBeNull();
}
patched() {
expect(this.spanRef.el).toBeNull();
}
}
const component = new TestRef(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>owl</span></div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
onWillPatch(() => {
steps.push("willPatch");
});
onPatched(() => {
steps.push("patched");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["willPatch", "patched"]);
});
test("patched, willPatch, onPatched, onWillPatch order", async () => {
const steps: string[] = [];
function useMyHook() {
onPatched(() => {
steps.push("hook:patched");
});
onWillPatch(() => {
steps.push("hook:willPatch");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
willPatch() {
steps.push("comp:willPatch");
}
patched() {
steps.push("comp:patched");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
});
test("two different call to willPatch/patched should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onPatched(() => {
steps.push("hook:patched" + i);
});
onWillPatch(() => {
steps.push("hook:willPatch" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div>hey2</div>");
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
});
describe("autofocus hook", () => {
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
test("simple input", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<input t-ref="input2"/>
</div>`;
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
test("input in a t-if", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<t t-if="state.flag"><input t-ref="input2"/></t>
</div>`;
state = useState({ flag: false });
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
component.state.flag = true;
await nextTick();
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
});
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new TestComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val");
expect(component.env).toHaveProperty("val");
});
test("parent and child env", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 5 });
}
}
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child };
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new Parent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>");
});
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
function useMyHook() {
onWillStart(() => {
steps.push("onWillStart");
});
onWillUpdateProps(nextProps => {
expect(nextProps).toEqual({ value: 2 });
steps.push("onWillUpdateProps");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useMyHook();
}
}
class App extends Component<any, any> {
static template = xml`<div><MyComponent value="state.value"/></div>`;
static components = { MyComponent };
state = useState({ value: 1 });
}
const app = new App(env);
await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(steps).toEqual(["onWillStart"]);
app.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
});
});
+189
View File
@@ -0,0 +1,189 @@
import { AsyncRoot } from "../../src/misc/async_root";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Env, Component } from "../../src/component/component";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
describe("Asyncroot", () => {
test("delayed component with AsyncRoot component", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("fast component with AsyncRoot", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<AsyncRoot>
<Child val="state.val"/>
</AsyncRoot>
<AsyncChild val="state.val"/>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("asyncroot component: mixed re-renderings", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`
<span t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>`;
state = useState({ val: 0 });
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
// click on each Child to increment their local counter
const children = parent.el!.querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
});
});
-321
View File
@@ -1,321 +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(1);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(2);
});
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);
expect(obj.a.__owl__.rev).toBe(1);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(2);
});
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
File diff suppressed because it is too large Load Diff
+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})"
);
});
});

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