Compare commits

...

595 Commits

Author SHA1 Message Date
Géry Debongnie 0924ff63d8 wip 2022-01-13 15:22:11 +01:00
Géry Debongnie fb64355f59 wip 2022-01-13 15:18:41 +01:00
Géry Debongnie f6c9f4f2bf wip 2022-01-13 15:15:47 +01:00
Jorge Pinna Puissant 00ffab1f64 [IMP] portal: portal as a Directive
Before this commit, portal was a Component, now is a directive.
This commit also clean some unused code, and fix an issue on the clean
optimization when a portal is found in a condition or a loop.
2022-01-13 15:02:11 +01:00
Lucas Perais (lpe) a226f92def [FIX] components: avoid leaks when children are outdated/destroyed
Every use case involving some sort of key set on a component would give birth to a leak in an async context:
- If a key of a component changed, the outdated one was never destroyed.
- destroyed component were never removed from their parent's reference map.

This commit solves both issues, that are tightly linked anyway.
2022-01-13 10:17:47 +01:00
Lucas Perais (lpe) 1749bce155 [FIX] test/helpers: useLogLifeCycle supports custom key 2022-01-13 10:17:47 +01:00
Géry Debongnie c00127fa1e [IMP] tooling: add testTimeout argument to test:debug command 2022-01-13 10:05:10 +01:00
Géry Debongnie 0b4b7899f6 [FIX] portal: properly handle errors
Before this commit, Portal overrode the _render function for its
component node, which means it bypassed the error handling mechanism
that was implemented in that method.  It could have been fixed by
duplicating the error handling code as well, but a better solution in my
opinion is to simply override the renderFn function.  This is closer to
the actual intent of the portal implementation: wrap the result of the
rendering in a VPortal vnode.
2022-01-12 08:51:28 +01:00
Lucas Perais (lpe) 5c324fa075 [FIX] app, components: dynamic t-call should propagate the key
Have a Component which has a Component node, and a dynamic t-call itself having
a Component node.

Before this commit, both children had the same `key`, (as in the key in parent.children, which registers on the parent all its children).

As a result, the scheduler was endlessly hanging.

After this commit, it works as expected.
2022-01-10 15:29:25 +01:00
Géry Debongnie 3945c009ed [REF] compiler: factorize a common pattern 2022-01-10 12:31:37 +01:00
Géry Debongnie 5b1132f01a [FIX] compiler: handle t-set as functions 2021-12-28 13:50:35 +01:00
Géry Debongnie f014b1a33e [DOC] update quick_start and how to test pages 2021-12-22 15:03:38 +01:00
Géry Debongnie 3f09a953b0 [REM] doc: remove overview page 2021-12-22 15:03:38 +01:00
Géry Debongnie b8e7ee5ff7 [DOC] update the tutorial todo app 2021-12-22 15:03:38 +01:00
Géry Debongnie 7fdca35a74 [FIX] reactivity: work even if no callback is given 2021-12-22 14:59:28 +01:00
Géry Debongnie 489e20843c [FIX] components: make sure t-ref work with t-if/t-else 2021-12-22 13:58:02 +01:00
Samuel Degueldre 34b781aeed [IMP] compiler: scope generated ids to their prefix
This means that unrelated ids (eg the id of a template, variable or key)
not longer share the same incrementing counter, meaning that you no
longer see a variable named "v2" unless another variable "v1" was
generated previously, this is also true for block data.
2021-12-22 13:30:09 +01:00
Géry Debongnie e8b0f31da6 [IMP] compiler: improve generated compiled code 2021-12-22 13:30:09 +01:00
Géry Debongnie d160c4a628 [FIX] useEffect: can depend on dom dependencies
Because the dependencies are now computed in patched.
2021-12-22 10:47:28 +01:00
Géry Debongnie 4542171a31 [FIX] portal: do not crash in dev mode
Before this commit, the props validation would fail in dev mode because
it did not expect a slot prop.
2021-12-21 21:43:46 +01:00
Géry Debongnie 0b1c4dd4ef [IMP] app: improve API, small refactoring 2021-12-21 12:12:48 +01:00
Géry Debongnie f4da50d350 [REF] build: do not output const enum definitions
We only use 2 const enums in the codebase, but they are defined in the
output, even though this is not useful in any way. This commit reduces
the final output by about 30 loc.
2021-12-21 09:09:36 +01:00
Géry Debongnie f2921abda8 [REM] component: remove support for css tag 2021-12-21 09:09:36 +01:00
Géry Debongnie 107200fd94 [IMP] components: improve props validation
to be able to specify that additional props are allowed
2021-12-21 09:02:45 +01:00
Lucas Perais (lpe) 0f8c859d5f [FIX] component: error_handling when an error is rethrown 2021-12-20 16:55:50 +01:00
Géry Debongnie cca8438d38 [REF] components: remove .el 2021-12-20 14:43:21 +01:00
Géry Debongnie a727347d60 [REF] tests: improve test helpers
- remove snapshotApp
- remove addTemplates
- simplify helpers
- make sure snapshotted templates are snapshotted with the app config
2021-12-20 12:43:36 +01:00
Lucas Perais (lpe) f19de73b0f [FIX] fiber, lifecycle: trigger a render during the fiber.complete
Have a component which does a render in its onWillPatch, onPatched, onMounted hooks.

Before this commit, the result was incorrect: the second rendering was not taken into account.

After this commit, those renderings are correctly applied at the price of a delayed render when the fiber
is in a critical state.
2021-12-20 10:54:51 +01:00
Géry Debongnie a8ffcafd23 [FIX] blockdom: properly handle falsy attributes
This commit fixes some issues with falsy attributes not being properly
set/removed in various situations. Also, the behaviour was not
consistent between normal attribute (key/value) and generic attributes
(pair or object)
2021-12-16 15:07:27 +01:00
Géry Debongnie a2f01ef4ad [FIX] components: improve error handling
Owl provides a way to manage errors occuring in component lifecycle
methods. However, before this commit, these errors were not always
logged or visible, which is very annoying in the common developer
workflow (doing something, checking it works, seeing no error but a
broken interface).

In this commit, we make sure errors are logged/throws in all cases:

- if an error occurs in a mounting operation => the promise is rejected
(which will log the error)
- if an error occurs after the mounting operation and is not handled by
any error handlers => the error will be logged (with console.error).
Also, in that case, this commit adds a warning to explain that owl
destroys the root component, which will help developers understanding
what happened.
2021-12-16 13:29:31 +01:00
Géry Debongnie d7de25e867 [FIX] compiler: properly handle <t> tags in some cases
The problem was that the compiler is based on the assumption that the
multi block received by the parser only occurs in some cases
where the structure of the template require a multi block, and it does
not work when we have random multiblock elsewhere.

We could fix the issue by modifying the code generator code to support
these usecases, or by simply removing these cases in the parser. Since
this seems more efficient, this is the approach taken by this commit.

Note that it was a good opportunity to simplify the parser.
2021-12-15 15:32:56 +01:00
Géry Debongnie 424bc480e2 [DOC] update changelog 2021-12-14 16:03:10 +01:00
Géry Debongnie 421f400766 [DOC] reorganize doc, unskip test, fix some links 2021-12-14 09:45:25 +01:00
Géry Debongnie c37f3445f6 [FIX] blockdom: ignore attributes with undefined value 2021-12-13 15:07:03 +01:00
Géry Debongnie 4cde06d685 [IMP] component: add .bind suffix to props for easy binding 2021-12-13 12:43:23 +01:00
Lucas Perais (lpe) 7b95315b87 adapt to fix and to fixup 2021-12-13 12:39:43 +01:00
Géry Debongnie 09761ddd8e fixup, maybe 2021-12-13 12:39:43 +01:00
Lucas Perais (lpe) b3c6ec2b48 [FIX] component: render in delayed willUpdateProps
Have a child component on which a render is triggered.
This component delays its willUpdateProps and makes a rendering during the willUpdateProps

Before this commit, renderings of the child were inconsistent across
its parent's renderings.

After this commit, it works as expected.
2021-12-13 12:39:43 +01:00
Lucas Perais (lpe) b20462ada6 [FIX] app: factorize to allow smoother developments in projects
It should be usefull to allow developpers to implement mounting/unmounting
if they wish to.
2021-12-13 09:49:48 +01:00
Lucas Perais (lpe) 25000b36be [FIX] blockdom: do not propagate svg namespace to siblings 2021-12-03 14:01:44 +01:00
Lucas Perais (lpe) 144d4a253c [FIX] hooks: useSubEnv supports arbitrary descriptors in env
Before this commit, when defining a getter in the env passed to useSubEnv,
the value was read, losing the definition of the property.

After this commit, declaring a getter in the env works as expected:
the property stays a getter.
2021-12-03 08:06:29 +01:00
Lucas Perais (lpe) 9d48bda227 [FIX] app: support for arbitrary descriptors in env
Before this commit, when defining a getter in the env passed to the App,
the value was read, losing the definition of the property.

After this commit, declaring a getter in the env works as expected:
the property stays a getter.
2021-12-03 08:06:29 +01:00
Lucas Perais (lpe) c8db663869 [FIX] parser: correctly parse pre node within a div with new lines 2021-12-03 08:03:45 +01:00
Lucas Perais (lpe) 247200194f [FIX] component: error handling in class inheritance
Before this commit, class inheritance when using the onError hook was unclear nay wrong.

After this commit, error handlers are called from the bottom up  in the inheritance hierarchy.
If a handler doesn't rethrow the error, the handling stops there and no other handler is called.
If a handler does rethrow, the handlers declared in a parent class are executed.
2021-12-01 17:29:26 +01:00
Géry Debongnie 60b8817ac0 [REF] compiler: simplify all compiled templates 2021-12-01 17:25:31 +01:00
Géry Debongnie e008966291 [FIX] compiler: allow t-if with empty content 2021-12-01 13:22:32 +01:00
Géry Debongnie e2fedc6eff [REF] components: unskip concurrency test 2021-12-01 13:22:32 +01:00
Géry Debongnie b3fb9a35bf [FIX] move error handling out of fiber, fix complicated mounted issues 2021-12-01 12:07:26 +01:00
Géry Debongnie 240259568e [FIX] component: improve error handling
In the following situation: A parent of B, B parent of C, with an error
when C is mounted, caught by B and retriggering a rendering in B, then
the onMounted hook of A wasn't properly called. This commit fixes this
problem.
2021-12-01 12:07:26 +01:00
Géry Debongnie 18fb67eaad [FIX] blockdom: properly handle references
Before this commit, there were situations where the reference numbers
were not properly set, which caused the blocks generated to crash
because the algorithm could not get correct references.
2021-12-01 09:34:55 +01:00
Géry Debongnie e946967867 [IMP] component: render does not return a promise anymore 2021-11-30 08:03:17 +01:00
Géry Debongnie 777b2aae5e [IMP] add support for top level comments 2021-11-29 15:36:45 +01:00
Géry Debongnie 0831bb54e3 [REF] component: remove onDestroyed, implement onWillDestroy 2021-11-29 14:59:01 +01:00
Géry Debongnie 03f34a38dc [TESTS] test lifecycle in reactivity tests 2021-11-29 08:42:47 +01:00
Géry Debongnie dcefd26bee [REF] tests: improve useLogLifecycle and helpers 2021-11-29 08:42:47 +01:00
Samuel Degueldre 922dab8e8f [FIX] reactivity: only call clearReactivesForCallback once on unmount 2021-11-26 16:22:38 +01:00
Samuel Degueldre 99740f9993 [FIX] reactivity: fix memory leak 2021-11-26 15:55:02 +01:00
Géry Debongnie 0b79b1c8fd [FIX] compiler: readd template name in compiled code 2021-11-26 09:17:49 +01:00
Géry Debongnie 5b0dce94cf [REF] code_generator: move generating code to CodeTarget
Before this commit, we had two places with code that generate a function
code. Now, all this code is moved in a method 'generateCode' on
CodeTarget.
2021-11-26 09:17:49 +01:00
Géry Debongnie a1d435c5a5 [FIX] compiler: call dynamic templates with correct this 2021-11-25 17:29:30 +01:00
Géry Debongnie 13c3178760 [FIX] slots: properly bind this in t-on arrow functions 2021-11-25 14:45:30 +01:00
Géry Debongnie 2b565ce1b4 [REF] component: small cleanup
This commit makes it simpler to understand the way fibers are assigned
to nodes.
2021-11-25 11:34:23 +01:00
Géry Debongnie cfcf2c6714 [FIX] component: concurrency issue
When a parent and a child were rendered at the same time, it was
possible for the 2 renders to decrement the same fiber internal
counter, which meant that the render was stalled.
2021-11-25 11:12:31 +01:00
Samuel Degueldre 92174559a1 [FIX] slots: allow t-call and components in slot default content 2021-11-24 15:47:51 +01:00
Bruno Boi 840348892f [IMP] owl: upgrade rollup-plugin-typescript2 to version 0.31.1 2021-11-24 11:45:12 +01:00
Samuel Degueldre b988a68b6f [IMP] parser: normalize document before parsing 2021-11-24 11:19:45 +01:00
Samuel Degueldre 7869a1f2c6 [IMP] components: add test for template string in props 2021-11-24 10:09:52 +01:00
Samuel Degueldre 9e81d14d50 [IMP] parser: throw when using unsupported directive on component 2021-11-24 10:09:52 +01:00
Samuel Degueldre 0e4a55ba09 [FIX] components: allow prop names that are not valid bare property name 2021-11-24 10:09:52 +01:00
Mathieu Duckerts-Antoine 6e9b68dafa [FIX] props: prop names can contain - 2021-11-23 10:20:45 +01:00
Bruno Boi ba7c9063c0 Update CHANGELOG.md 2021-11-23 10:09:39 +01:00
Mathieu Duckerts-Antoine 3a361876ad [IMP] slots: via prop 'slots'
The slot inner working has been reworked. A prop "slots" is now passed
explicitely to the component. It looks like

{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }

with the objects slotInfo_i with mandatory keys "__render", "__ctx",
and optional key "__scope" and possibly others.

Here is how a slotInfo object can be created:
A slotInfo object is normally created by setting in a template something
like

<div>
    <t t-set-slot="foo" t-set-scope="scope" param_1="var" param_2="3">
        content
        <t t-esc="scope.bool"/>
        <t t-esc="scope.num"/>
    </t>
</div>

and it will be used somewhere like

<div>
    <t t-esc="props.slots.foo.param_1"/>
    <t t-slot="foo" bool="other_var" num="5">
</div>

In the above example, the function "__render" produces the block dom
element for the content of the t-set-slot.
The context "__ctx" will have a key "scope" with value { bool: ..., num: 5 }
and "__scope" will be set to "scope".
2021-11-22 16:36:43 +01:00
Samuel Degueldre 58b8572f0a [FIX] components: capture context in prop expressions 2021-11-22 13:18:17 +01:00
Samuel Degueldre c23637e6d8 [FIX] components: throw on duplicate t-key instead of hanging the app 2021-11-22 11:11:49 +01:00
Géry Debongnie 4a4b1fbba5 [IMP] app: add templates in app config
Also, improve the parsing code
2021-11-22 10:56:16 +01:00
Géry Debongnie 536d9e1762 [REM] tools: remove benchmarks/debug script
They are either no longer relevant, or less useful than some
alternatives (such as the js framework benchmark project)
2021-11-22 10:56:16 +01:00
Lucas Perais (lpe) 6ed68372a6 [FIX] package: bump owl version to 2.0.0-alpha1 2021-11-22 10:49:51 +01:00
Lucas Perais (lpe) efd934d2b1 [FIX] tools: adapt playground to owl 2 2021-11-19 16:11:28 +01:00
Lucas Perais (lpe) dabc24cee3 [FIX] index, reactivity: export reactive function in index 2021-11-19 16:11:28 +01:00
Samuel Degueldre 7f49796a07 [IMP] misc: update typescript to 4.5.2 2021-11-19 14:42:37 +01:00
Géry Debongnie 757dffefac [IMP] components: rename onRender->onWillRender, add onRendered 2021-11-19 13:26:44 +01:00
Samuel Degueldre bdfe058279 [IMP] reactivity: overhaul reactivity system
This commit makes the reactivity system more fine grained and makes it
more eager to stop observing keys or objects when they are modified,
this results in fewer "false positive" notifications.
2021-11-19 13:20:37 +01:00
Bruno Boi 345c44b952 fixup! [IMP] svg namespace support 2021-11-19 12:47:51 +01:00
Bruno Boi ea74739d46 [IMP] svg namespace support 2021-11-19 11:55:18 +01:00
Géry Debongnie 93b53d8017 [FIX] remove cyclic dependency, improve error typing (#982) 2021-11-18 10:44:49 +01:00
Lucas Perais (lpe) c2284bc6f5 [FIX] component, fiber: error_handling at the Fiber level
Before this commit, errors triggered at the level of the fiber (as opposed to at the level
of a component's rendering), were handled as the very top level of the rendering, that is,
in the scheduler.
This was wrong because components below in the rendering tree would not have a chance to handle their
children's or their own errors.

After this commit, error triggered in willPatch, onMounted and onPatched are correctly handled
at the closest component to where they were thrown.
2021-11-17 11:40:29 +01:00
Géry Debongnie 48650da62e [REM] remove some outdated tests 2021-11-17 09:21:55 +01:00
Géry Debongnie 2edd6cb8f3 [MOV] move memo and portal to root folder 2021-11-17 09:21:55 +01:00
Géry Debongnie 4b0a37c542 [REM] tests: remove async root tests 2021-11-17 09:21:55 +01:00
Lucas Perais (lpe) 50c0e30936 [FIX] component: error_handling on current component
Have a Child Compnent which has one component that succeeds and another
one that fails at its instanciation.
The Child component handles the Errors by rendering itself.

Before this commit, the error handling algorithm made impossible for the scheduler to finish.
This was because the current fiber was still counted as ongoing, when it was actually completed.

After this commit, this use case is handled correctly.
2021-11-16 17:05:25 +01:00
Géry Debongnie e97335d7ab [DOC] add a change log 2021-11-16 16:36:20 +01:00
Lucas Perais (lpe) 6ea40a66da [IMP] app, compiler: introduce t-out
t-out automatically escaped content when it is a string not marked
with the `markup` function

t-out renders the raw content if it is a Block, or if it has been marked
with the `markup` funtion.

t-esc has been kept since it is safe and is optimized to render text nodes.

all t-raw calls are in fact the same as t-out.
2021-11-16 14:37:53 +01:00
Lucas Perais (lpe) 2806cda420 [FIX][BREAKING] t-esc on component is not supported anymore 2021-11-16 14:37:53 +01:00
Bruno Boi cb0b525f32 [IMP] templates: can load multiple at once and also from XMLDocument 2021-11-16 13:10:09 +01:00
Mathieu Duckerts-Antoine 30d28670e8 [REF] reactivity: new API 2021-11-15 17:14:19 +01:00
Mathieu Duckerts-Antoine 7b761a2f8b [REF] tests: remove debugger 2021-11-15 17:14:19 +01:00
Mathieu Duckerts-Antoine d834bb2ac4 [FIX] reactivity: memory leak
The deletion of a key in an observed object did clear the observers of
that key but did not clear the atoms created (if any) when the key existed
(e.g. on the key value if it was trackable). This can lead to a growing
set of atoms that are useless but kept in memory if a lot of keys are
added/deleted. The same thing can happen if a key value is changed many
times and the values are trackable.

Here we fix the problem by
- keeping tracks of the objects that have been observed by an external call
  to the method "atom" (we call them seeds).
- remove all observer atoms that are not seeds for observers of at least
  one key deletion or key value change.

Note the fix is in some sense partial: a user could use the "atom" primitive
making the new system uncapable of avoiding a leak (see test "atom on an
object with a getter 3" where a getter is used in a weird way in an
observed object).
2021-11-15 17:14:19 +01:00
Bruno Boi 6ae92fa4b3 [IMP] hooks: reintroduce useExternalListener 2021-11-15 16:15:14 +01:00
Bruno Boi 3ceb118ace [IMP] hooks: introduce useEffect
Co-Authored-By: Samuel Degueldre <sad@odoo.com>
2021-11-15 16:07:25 +01:00
Bruno Boi fa426b7fc0 [IMP] owl: mount util now takes an AppConfig 2021-11-15 16:07:25 +01:00
Bruno Boi d7403871fc [IMP] env: env is now frozen, useSubEnv does not affect user env 2021-11-15 13:21:46 +01:00
Géry Debongnie 6aeff21d9a [FIX] tests: remove useless log 2021-11-15 09:35:13 +01:00
Géry Debongnie 429e145b68 [FIX] portal: unskip some tests 2021-11-15 09:35:13 +01:00
Géry Debongnie fb6aab8b70 [FIX] component: fix lifecycle order 2021-11-15 09:35:13 +01:00
Géry Debongnie 9e285e1e0a [FIX] unskip tests 2021-11-15 09:35:13 +01:00
Géry Debongnie 63bfdfb8db [REF] move event_bus into utils, readd 2 functions 2021-11-15 09:35:13 +01:00
Géry Debongnie d4c4fe853e [REM] tests: remove debug script tests 2021-11-15 09:35:13 +01:00
Géry Debongnie 9163bb1e08 [REF] app: move TemplateSet into its own file 2021-11-15 09:35:13 +01:00
Géry Debongnie a187a376a0 [REF] move app and compiler code around 2021-11-15 09:35:13 +01:00
Géry Debongnie 06ea6d2490 [REF] component: fix typescript error 2021-11-15 09:35:13 +01:00
Géry Debongnie 04334e23d7 [REF] compiler: rename qweb/ into compiler/ 2021-11-15 09:35:13 +01:00
Géry Debongnie 4efe1ae166 [IMP] reactivity: slightly improve code and typing 2021-11-15 09:35:13 +01:00
Lucas Perais (lpe) ad4673cb36 [IMP] component: re-introduce error handling in lifecycle 2021-11-12 16:54:40 +01:00
Samuel Degueldre e611c20ab4 [IMP] qweb: turn handlers into function expressions only
For the sake of consistency with vanilla JS, and to allow some things
that were previously not possible.
2021-11-12 13:17:43 +01:00
Mathieu Duckerts-Antoine d60a5a414e [IMP] reactivity: Context replacement
Aim to replace the abstraction "Context" from OWL 1 with the new primitives
"atom" and "useState":

- notification is done only after a batch of modifications.
- observers are notified at most once for a batch.
- an observer of type component is notified (and rerendered)
  only if it does not have an ancestor that has to be notified for the
  same batch of operations (anywhere in the web of references!).
- notification of components is done on all levels "simultaneously".

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
2021-11-10 15:08:08 +01:00
Géry Debongnie d47dcf6be2 [IMP] pin prettier version to ensure consistent results 2021-11-10 14:03:17 +01:00
Samuel Degueldre 3e70b17da1 [FIX] qweb: fix crash with ref an component in same slot 2021-11-10 13:36:13 +01:00
Samuel Degueldre c718d8e6c6 [FIX] qweb: fix crash when component only renders empty slot 2021-11-10 13:36:13 +01:00
Samuel Degueldre a122a94180 [IMP] qweb/components: remove t-ref on components
Refs to component expose a lot of implementation details that should be
private to parents. Parent to child communication should go through
props.
2021-11-10 13:36:13 +01:00
Bruno Boi ca139166ab [IMP] qweb: reintroduce t-tag directive
will not be compatible with t-model directive !
2021-11-09 16:36:03 +01:00
Lucas Perais (lpe) ae71db28e4 [IMP] qweb: compiler: support t-key on node and component without t-foreach 2021-11-09 14:53:19 +01:00
Bruno Boi 779fc6b02b fixup! [IMP] bdom: support multiple synthetic events on one node 2021-11-08 13:36:09 +01:00
Bruno Boi 84913593b2 [IMP] bdom: support multiple synthetic events on one node 2021-11-08 13:36:09 +01:00
Bruno Boi 8d4eb21536 [IMP] qweb/attributes: uncomment two tests
- textarea with t-att-value
- select with t-att-value
2021-11-08 11:25:14 +01:00
Bruno Boi 4bdfaf96f3 fixup! [IMP] qweb: introduce t-model directive 2021-11-03 15:25:21 +01:00
Bruno Boi 233c953243 [IMP] qweb: introduce t-model directive
supported modifiers: lazy, trim, number
2021-11-03 15:25:21 +01:00
Bruno Boi ee5ad354ff [IMP] tags: reintroduce inline css tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
  static template = xml`
        <div class="my-component">some template</div>
    `;
  static style = css`
    .my-component {
      color: red;
    }
  `;
}
```

The `css` tag registers internally the css information. Then, whenever the first instance of the component is created, will add a <style> tag to the document <head>.

Original commit in Owl v1: 953778dc5
2021-11-03 10:07:14 +01:00
Mathieu Duckerts-Antoine 4e4b85e2a9 [IMP] reactivity: new primitives for reactivity
fine grained reactivity:

existing key in source changes --> only observer having read the key are notified

add/delete key in source changes --> all source observers are notified

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
2021-10-29 14:52:20 +02:00
Mathieu Duckerts-Antoine 6479631983 Code prettification 2021-10-29 14:52:20 +02:00
Lucas Perais (lpe) 2a0c410014 [IMP] qweb, blockdom, components: t-on with modifiers
supported modifiers: capture, prevent, stop, self.
2021-10-28 13:38:56 +02:00
Lucas Perais (lpe) bb4aecf638 [IMP] blockdom: t-on supports synthetic and native event handler
Synthetic handler is a sort of event delegation that allows placing
only one listener on the document to improve performance. It is an opt-in option.

Native listener places the listener on the node itself.
2021-10-28 13:38:56 +02:00
Lucas Perais (lpe) a1adfd5a1b [FIX] qweb, component: remove support for t-on on component node 2021-10-27 13:38:35 +02:00
Lucas Perais (lpe) dd4848f602 [FIX] qweb: t-key in t-foreach is mandatory, throws otherwise 2021-10-27 13:38:35 +02:00
Bruno Boi b25f476c22 [FIX] qweb: reintroduce test on t-debug 2021-10-26 09:33:56 +02:00
Mathieu Duckerts-Antoine 90cdb97b49 [IMP] tests: component mounting
We re-add some tests for component mounting.
2021-10-26 09:05:33 +02:00
Mathieu Duckerts-Antoine 7d7568d254 [IMP] app: mount app in "first-child" position
We reintroduce the possibility to mount the app in first position in
a target. The option "self" has been dropped since it is now possible
for a component to have several top level nodes.
2021-10-26 09:05:33 +02:00
Bruno Boi 06d207f60e [IMP] package.json: add watch arg to test:debug command
Before this commit
The command "npm run test:debug" runs the tests once.

After this commit
Jest runs in watch mode
2021-10-22 15:57:23 +02:00
Bruno Boi ff564421da [IMP] package.json: add remote test:debug command
Usage:
Open chrome://inspect then run in console:
> npm run test:debug ./path/to/your/testfile.ts
2021-10-22 14:51:23 +02:00
Mathieu Duckerts-Antoine c3695ec8db [IMP] component: defaultProps application
We re-add the application of defaultProps. Note that the application
is done twice in dev mode.
2021-10-22 12:46:57 +02:00
Mathieu Duckerts-Antoine 6e0834ce5e [IMP] component,qweb: props validation
We re-add the possibility to validate props when dev mode is active.
No change in the API right Now. The dev mode is activated via the
configure method of App class.
2021-10-22 12:46:57 +02:00
Samuel Degueldre d1425c7100 [FIX] component: correctly create a new node when previous is destroyed
Previously, when a component node had been created and destroyed, and
the corresponding component was then recreated, the destroyed node was
reused. This commit fixes that
2021-10-22 10:00:35 +02:00
Samuel Degueldre e01fe301c0 [IMP] *: re-add a bunch of tests 2021-10-21 10:34:54 +02:00
Mathieu Duckerts-Antoine ba2fe3ff55 [IMP] qweb: t-props directive
We reimplement the directive "t-props" and add some tests for it.
2021-10-20 15:52:43 +02:00
Géry Debongnie 700d574314 [MOV] move lifecycle_hooks into component/ 2021-10-20 15:16:28 +02:00
Mathieu Duckerts-Antoine ba32772a92 [FIX] qweb: re-add test on memory leak
Re-add test from 6e185f9.
2021-10-20 15:16:26 +02:00
Mathieu Duckerts-Antoine e4a0277f68 [FIX] qweb: t-set directive
This commit reintroduces some tests for the t-set directive and make
them pass. For that, it was necessary to adapt the qweb compiler in
order to get the following behaviors:

A t-set can affect parent contexts (up to the first parent tagged as
boundary) when the key changed is found in one of the parent contexts.
Some context are marked as boundaries in such a way that

   - rendering contexts (e.g. components) cannot be modified via a t-set.
   - a t-set in a t-call body or in a called template can never change a
     context above the t-call context.

Code prettification has been done.
Snapshots have been modified.
2021-10-20 15:16:25 +02:00
Lucas Perais (lpe) abe3748825 [FIX] re-introduce tests 2021-10-20 15:16:24 +02:00
Mathieu Duckerts-Antoine ee64de8b3f [IMP] qweb: throw error when t-component is not used with a 't' tag 2021-10-20 15:16:22 +02:00
Lucas Perais (lpe) ed831320db [FIX] re-introduce some missing tests
The point is to have visibility on the development of the owl2 features.
This commit reintroduces some tests keeping them skipped in order to fulfill that purpose.

There still are some missing tests though.
2021-10-20 15:16:21 +02:00
Mathieu Duckerts-Antoine 5f1e1e189f [IMP] qweb: re-add support of t-on directive
We add some test for the t-on directive.

For making them pass, it was necessary to change the code produced by
compileTForeach: the const declaration is not done by using generateId
and there was some conflict with the variable names produced in
captureExpression. Consequently, many snapshots had to be changed.

Code prettification has been done too.
2021-10-20 15:16:20 +02:00
Mathieu Duckerts-Antoine 55ef9ca116 [ADD] components: re-add a test for t-foreach directive 2021-10-20 15:16:18 +02:00
Samuel Degueldre 642ecf0ccd [IMP] components: re-add a bunch of components tests from owl 1
Some tests are skipped because they rely on not-yet-implemented
features.
2021-10-20 15:16:16 +02:00
Samuel Degueldre c9cc789f1d [IMP] components: add back style_class tests
The tests have been adapted to the new way of doing things, some tests
are skipped because they rely on features that are not implemented yet
2021-10-20 15:16:14 +02:00
Mathieu Duckerts-Antoine b8649f1add [ADD] Translation feature
This commit brings back the possibility to translate text nodes and
the attributes "label", "title", "placeholder", and "alt" in an app
configured with a suitable translation function.

It is also possible to deactivate translations under a node via
the directive t-translation="off".

For flexibility it is possible to define the list of translatable
attributes in the app.
2021-10-20 15:16:12 +02:00
Samuel Degueldre e2d98e4c26 [REF] doc: remove references to router and store
Owl 2 will not have a router or store implemented inside the library
2021-10-20 15:16:06 +02:00
Samuel Degueldre e6bb4ef286 [REF] qweb: use native Node and Element instead of custom Dom types 2021-10-20 15:11:32 +02:00
Géry Debongnie 7ac20f4fc2 [REF] initial prototype of owl 2 2021-10-20 15:11:25 +02:00
Géry Debongnie 4e3b7c74da [REL] v1.4.7
v1.4.7

fix: memory leak in some templates
2021-10-19 16:47:20 +02:00
Géry Debongnie 98bb2842d8 [FIX] run prettier on codebase 2021-10-19 16:45:04 +02:00
Géry Debongnie 6e185f987b [FIX] qweb: do not leak values in global context in rare cases
In some very rare cases (such as the use of the t-foreach directive),
Owl did leak the values in the render context in the global context.
This was due to the fact that the compiled template looked like this:

let _3 = _4 = _5;

instead of

let _3 = _5;
let _4 = _5;
2021-10-19 16:41:45 +02:00
Géry Debongnie c0f495661d [REL] v1.4.6
# v1.4.6

- fix: crash in component render (in rare cases)
- fix: build system now target ES2017 instead of ESNext
- fix: remove useless log in prod mode
2021-10-04 15:16:01 +02:00
Géry Debongnie 0f7a8289a6 [FIX] config: do not log anything if in prod mode
It is worse having a dev mode in a production environment than the
opposite. The purpose of the warning was to avoid that situation.

However, in prod mode, it is not really necessary to log the config key.

closes #915
2021-10-04 14:57:30 +02:00
Géry Debongnie 1c3b04f6a8 [FIX] build: fix typescript target to ES2017
This makes sure that we do not leak modern code in odoo.
2021-10-04 14:57:30 +02:00
Géry Debongnie 64db7777dd [FIX] component: async issue
This is a tricky commit. The key point is that the Fiber.complete
method, which commits a rendering to the DOM works like this: it
traverses the component tree, patch the corresponding DOM for each
component, calls the mounted/destroy hooks, and reset the currentfiber
of components to null, all synchronously.

However, this means that it is possible for components to initiate a
rendering (which create a new currentFiber) before the currentFiber is
reset to null, so the internal state of owl is corrupted. This can
occurs in a crash, as in the test that accompanies this commit.

To fix this, we take care of resetting the currentFiber first, while we
walk the component tree. Then, the internal state is always consistent
(i.e. a currentFiber to null means that there is no pending rendering)

closes #904
2021-10-04 14:57:30 +02:00
Géry Debongnie 37313c47a3 [REL] v1.4.5
# v1.4.5

- qweb: expose translatable attributes
- ci/tests: update jest/rollup
- doc: clarify tutorial
- doc: update readme, add nice badges
2021-09-22 16:34:02 +02:00
Géry Debongnie 3f563de9c0 [CLEANUP] run prettier on codebase 2021-09-22 16:29:59 +02:00
Géry Debongnie 97564a7612 [FIX] tests: update tests error messages
Updating my node version to v16, I noticed many tests breaking, because
the way errors are formatted did change.  This commit ensures that the
test suite keeps working for all node versions.
2021-09-22 16:00:37 +02:00
Géry Debongnie f1abf7f2ea [DOC] update readme, to add nice badges 2021-09-21 09:36:15 +02:00
Géry Debongnie 8464a1b04e [IMP] qweb: expose translatable attributes
This commit allows owl users to add any attribute to the list of
translatable attributes.

closes #903
2021-09-21 09:36:15 +02:00
Géry Debongnie c0a62dfd05 [DOC] clarify tutorial
The part about setting up the various files did not express clearly that
we need the iife file, not the cjs.
2021-09-21 09:36:15 +02:00
Lucas Lefèvre 600f1e35d4 [IMP] package.json: update jest and rollup
If we want to upgrade Typescript to version >4 (in this repo or another using
owl), we also need jest >25. Jest <26 do not support Typescript >4.

However, jest >26 has a few breaking changes that completely breaks owl in
tests.

This commit updates jest to the current latest version (27) and adapts the
code accordingly.

A few words on what changed
---------------------------

1. the default test environment is no longer `jsdom`. It is now manually
   configured to restore the previous env.

2. the jsdom version has been upgraded. This brings a few breaking changes,
   detailed later.

3. there's a bug in jsdom >16.4. Manually created `<t/>` (by qweb compilation)
   are recognized as "T" and not "t". Qweb thinks it's a component (since the
   first letter is capitalized), but it's not a component: boom, everything
   breaks.
   A fix has been proposed here jsdom/jsdom#3240. But it's not likely to land
   in jest in the short term (jest would need to update its dependency to the
   next major jsdom version). This commit works around the problem for now by
   creating `<t/>` slighty differently such that they are in an XML document
   from the start (and not HTML document).

4. the xml parser implementation changed to increase the strictness and
   correctness of XML parsing, according to specifications. A few tests needed
   to be adapted.
   https://github.com/jsdom/jsdom/commit/c96decf837ece54bdc550dfb7dca7e5d6c97bc2d

5. jest matcher `toHaveProperty` now check inherited properties. This breaks a
   few tests. Since the breaking assertions didn't bring a lot of value from a
   behavior point of view (it was more "white box" technical tests), they are
   removed in this commit
   https://github.com/facebook/jest/commit/1256f76a5a83034b51c7524142b60b099f69a7ab

6. jsdom now implements the behavior of links (`<a/>`). One test was `click`ing
   on such a link, with the right click. Jsdom now tries to navigate to the
   pointed URl...but crashes because navigation is not implemented :(
   It turns out the test is probably not a valid/realistic scenario. On every
   tested browser (chrome - chromium - brave - edge -firefox - safari), a
   right click with the mouse triggers a `contextmenu` event and no `click`
   event.
   https://github.com/jsdom/jsdom/commit/cc95abc576f596ff7f3eaf8245f376e1f21aa485
2021-09-08 11:47:50 +02:00
Géry Debongnie b3181f119d [REL] v1.4.4
# v1.4.4

fix: overlapping multi-class in t-att-class
2021-09-03 09:57:00 +02:00
Géry Debongnie 1e1e05350a [REF] run prettier on codebase 2021-09-03 09:56:09 +02:00
Samuel Degueldre b242230e20 [FIX] qweb: fix overlapping multi-class in t-att-class
Previously, if two attributes in t-att-class shared some classes, their
presence would be determined by the last attribute declared, instead of
being present if any attribute containing it evaluates to true. This
commit fixes that.
2021-09-01 11:58:45 +02:00
Géry Debongnie 9fe2da704e [REL] v1.4.3
# v1.4.3

- fix: another scoping issue with t-slots
2021-07-08 11:54:32 +02:00
Géry Debongnie 21b1661d39 [CLEANUP] run prettier on codebase 2021-07-08 11:53:48 +02:00
Géry Debongnie ec05b1f5e3 [FIX] slots: fix bad interaction between t-call and slots
The previous changes in the combine method (used to copy all the
variables defined in the current scope for use in a slot) had the effect
of squashing the prototype chain: instead of `Component -> Obj1 ->
Obj2 -> Obj3`, the combined scope had: `Component -> Obj1'`.

This has an unfortunate interaction with the way t-call is implemented,
which uses the fact that we are in a subscope to add a own
__access_mode__. It depends specifially on the prototype chain, and that
the parent scope may have a different value for that property. But with
the way combine was implemented, we lost all that subtlety since
everything is squashed.

In this commit, we reimplement that function in a way to make sure we
keep the prototype chain structure
2021-07-08 11:49:25 +02:00
Géry Debongnie 8083678f03 [REL] v1.4.2
# 1.4.2

- qweb: properly handle inline expressions with lists such as '[a,b,c]'
2021-07-07 14:56:46 +02:00
Géry Debongnie 61c2ec5d83 [FIX] qweb: properly handle lists in inline expressions
Before this commit, Owl inline expressions with a list with multiple
elements such as [a,b,c] was transformed into

[scope['a'], b: scope['b'], scope['c']]

instead of

[scope['a'], scope['b'], scope['c']]

This is due to a previous commit adding support for short object
descriptions such as {a,b}.

To fix this means that we have to keep track of the current group type
for the expression, which is done by using a stack.
2021-07-07 14:50:27 +02:00
Géry Debongnie e579a993fd [REL] v1.4.1
# 1.4.1

This release brings in two small fixes:

- vdom: a performance improvement to reduce number of calls to classList.remove
- slots: fix subtle issue with wrong context used in event handlers when
  multiple slots are involved
2021-07-07 11:10:05 +02:00
Géry Debongnie ea3c6f7bf0 [FIX] slots: make sure handlers are properly bound to component
In some situations (a slot inside a slot), the combine method was
wrongly copying all properties of the scope into the context, which
caused the event handling system to wrongly use a subobject as component
(since it detects the fact that __owl__ is a own property_).
Consequently, we could have very subtle issue with some properties being
shadowed by a sub object.
2021-07-07 11:04:25 +02:00
Pierre Paridans 9cfafc30b5 [IMP] vdom: performance improvement
Port from original snabbdom project: snabbdom/snabbdom#634

The issue is that before this commit, the removeClass method was
sometimes called even if it is not useful. See this comment for more
detail: https://github.com/snabbdom/snabbdom/issues/633#issue-618706258
2021-07-06 14:14:40 +02:00
Aaron Bohy ad42c583c6 [FIX] package.json: bump node version to v12
Node 10.x is no longer maintained, and since a recent commit [1],
a test fails with that version.

[1] 88fd1cf483
2021-07-06 13:30:17 +02:00
Géry Debongnie 6faaa6c361 [REL] v1.4.0
# v1.4.0

This release is larger than usual: lot of small fixes, and a few improvements as
well.

- config: change debug mode warning to info
- fix: add support for callable expression in event handler (for example, `t-on-click="some.function"`)
- fix: allow specific props to override generic props (in `t-props`)
- fix: add support for short object description in inline expressions: `{ hey }`
- fix: add support for t-att-value on `<textarea/>` and `<select/>`
- fix: slots: issue with scoping in nested loops
- fix: slots: default slot is not lost in some cases
- fix: do not shadow an error in case the finalizing code fails
- imp: qweb: allow multiple class in class attribute object notation: `t-att-class="{'a b': value}"`
- imp: better detection for dynamic component change (less need for `t-key`)
- fix: router: allow querystrings in path
- fix: qweb: add support for template strings
- fix: qweb: make sure nodes with t-transition are removed in all cases
2021-07-06 10:27:55 +02:00
Géry Debongnie bff539cabf [FIX] tooling: make sure we escape content in release script 2021-07-06 10:27:30 +02:00
Joseph Caburnay 88fd1cf483 [FIX] extensions: guarantee to call transitionend callback
There is a possibility that the transitionend event of an element/component
with t-transition directive won't trigger. Though this situation is
difficult to assert, it was observe in odoo runbot for the pos ui.

When the transitionend event is not fired, the callback that removes
the element from the dom won't be called, resulting to a corrupted view.
An example of which is the following:

```html
<div t-if="show" t-transition="fade">Hello</div>
```

If `show` is set to false by some ui action and by any chance the
transitionend event is not fired (perhaps because the transition didn't
actually start or because of completely unknown reason), the div element
will remain in the view -- and this is not desirable.

This commit patches this situation such that if after 50ms that the
transitionend event is supposed to be fired but the event isn't fired, we
force the callback using a setTimeout. This guarantees the call of the
callback that is suppose to remove the element from the view.
2021-07-06 09:11:12 +02:00
Russell Briggs f0b75b6890 [IMP] router: allow query string in paths 2021-07-06 09:05:58 +02:00
Géry Debongnie a1552117f9 [IMP] qweb: add support for template strings
in inline expressions, such as t-esc or t-value.

closes #746
2021-07-05 10:09:23 +02:00
Géry Debongnie 17ae1d06c4 [IMP] component: better detection for dynamic component change
Before this commit, Owl could not detect that the underlying component
in a template such as <t t-component="{{state.child}}"/> was changing,
if the two components have the same tag as root element.

This is because the reconciliation is done at the vdom level, which does
not know about components.  To solve this, one could use a t-key to make
sure owl can make the difference.

With this commit, we can simply use our knowledge of the fact that we are
dealing with a dynamic component and autogenerate a suitable key.

closes #623
2021-07-05 10:09:23 +02:00
Géry Debongnie 27629cedfa [IMP] qweb: allow multiclasses in t-att-class object form
the low level method htmlelement.classList.add does not accept multiple
classes in one string, which is why, in owl, the expression

`<div t-att-class="{'a b c': value}" />`

did not work as one might expect. It is however very convenient in real
life templates, so this commit improve owl by adding support for this
feature.

closes #813
2021-07-05 10:09:23 +02:00
Géry Debongnie 3a93370ab6 [FIX] component: do not shadow the initial error in some cases
Before this commit, the error handling code simply destroyed the
application whenever an unhandled error occured in the owl rendering
process. This is perfectly fine, except that since the application is
potentially corrupted, the destroy code may crash as well. We simply
catch those errors to avoid shadowing the main issue.

closes #866
2021-07-05 10:09:23 +02:00
Géry Debongnie a6bb4d8ee1 [FIX] component: make sure default slot is applied
Before this commit, owl was erroneously defining default slots in most
cases, even though they are empty. The problem occurs when the content
of a component slots is a t-set-slot, and we remove that, then use the
rest as default slots, even though it is only reduced to a set of text
nodes.

With this commit, we only consider the content as default content if it
is not only a sequence of text nodes containing only spaces.

closes #882
2021-07-05 10:09:23 +02:00
Géry Debongnie caf842c482 [FIX] component: fix scoping issue in nested loops
The templates contained in a slot have to be rendered with the current
scope, but during the rendering of the sub component, which happens
later (after the willStart for the sub component). Therefore, we need to
save the scope that should be used for the slots, so we can access the
proper variables. This was done using an Object.assign() statement,
which is actually only a shallowclone for the own properties.

In this commit, we properly copy all the properties for the current
scope, even those contained in the prototype.

closes #855
2021-07-05 10:09:23 +02:00
Géry Debongnie 1b513a1637 [FIX] qweb: add support for t-att-value on <select> tags
Before this commit, it was not possible to set the value of a select tag
by using the t-att-value attribute.

Doing so is not actually trivial because of the way the vdom works: it
processes the node attributes before its children are created, which
means that the vdom code tries to set the initial value of the select
before its children are created, which means that it is ignored. To make
it work, I added a node create hook which is called after the children
are completely processed.

closes #873
2021-07-05 10:09:23 +02:00
Géry Debongnie 52a200878a [FIX] qweb: add support for t-att-value on textareas
The code did not treat textareas in the same way as inputs. As a result,
using t-att-value on a textarea did not work.

closes #872
2021-07-05 10:09:23 +02:00
Géry Debongnie f4994a20d8 [MISC] owl: add temp/ in gitignore
I sometimes use the temp folder to keep stuff in while working
on refactorings/switching branches.
2021-07-05 10:09:23 +02:00
Géry Debongnie 7419c1982a [DOC] tags: add missing information
The example given in the tags page was misleading.

closes #884
2021-07-05 10:09:23 +02:00
Bishal Pun a8c8f6affe Update comparison.md 2021-07-03 07:27:47 +02:00
Géry Debongnie dabe7f9bfa [FIX] qweb: properly parse short object description
Before this commit, using an expression such as "{machin}" was compiled
in qweb into "{scope['machin']}" which is not valid.

With this commit, we instead transform it into "{machin:
scope['machin']}".

closes #885
2021-07-02 14:20:13 +02:00
Michael Mattiello (mcm) d08ea63565 [IMP] directive: make props override t-props
Before this commit, props and t-props were computed like
`Object.assign(props, t_props)`.
Now, it computes like `Object.assign(t_props, props)` so props will
override t-props.

closes https://github.com/odoo/owl/issues/886
2021-07-02 11:50:23 +02:00
Sébastien Theys 7ebe0da962 [IMP] component: add support for callable expression in event handler 2021-07-01 12:26:23 +02:00
Xavier Morel 3e0e8475a6 [CHG] config: flip debug mode warning to info
Backport of odoo/odoo@e4834fce69

There's no way to easily disable / silence this warning, because
multiple tours explicitly opt into debug mode (with good reasons). So
it's not enough to bypass the `mode` setter in the test setup helpers
and set `QWeb.dev` directly (which works for test_main in POS and
main_tests in web), there would also need to be special workarounds in
the 4 modules which set `owl.config.mode` based on the session's debug
mode.

I'm not sure how useful this warning is: it defaults to `false` so the
only situation in which this would be relevant would be for a
third-party to use Owl *and* explicitly enable the debug mode
in-source *and* forget to remove it when deploying to production *and*
look at their console.
2021-06-29 08:43:20 +02:00
Géry Debongnie 2e83c739b9 [REL] v1.3.2
# v1.3.2

Fixes

- components: correctly propagate errors to parent
2021-06-18 10:41:51 +02:00
Géry Debongnie 922eb7cd98 [FIX] component: propagate errors to parent
Before this commit, the error handling process was too naive: once an
error occurs in a rendering, owl catches it, looks for a component that
implements the catchError method, then calls it.

However, in real life, we sometimes need to rethrow that error (or
another one) to propagate the error to some parent handler. This error
needs to be handled by the closest parent component that implements
catchError.

This is what this commit implements: it wraps the catchError call in a
try/catch, then in case of errors, try to handle it by a parent.
2021-06-18 10:34:09 +02:00
Géry Debongnie d12115554c [REL] v1.3.1
# v1.3.1

Fixes

- components: fix issue with t-call in nested t-slot (parent component was incorrect)
2021-06-10 10:36:33 +02:00
Géry Debongnie 1a6b26c8c6 [REF] cleanup: prettier 2021-06-10 10:23:48 +02:00
Lucas Perais (lpe) d67d295eaa [FIX] qweb, t-call: t-call nested in t-slot
Have a t-call within a t-set-slot of a component.
The called template has a t-component directive.

It should be like:

```xml
<t t-name="Zero">
  <Slotted>
    <t t-call="someTemplate" />
  </Slotted>
</t>

<t t-name="someTemplate">
  <SomeComponent />
</t>
```

Before this commit, the parent of SomeComponent was the Zero component

After this commit, the parent of SomeComponent is the Slotted Component as it should be

closes #862
2021-06-10 10:00:22 +02:00
Géry Debongnie 9cbcf20b33 [REL] v1.3.0
# v1.3.0

## Improvements

- qweb: add support for  directive
2021-06-04 14:19:07 +02:00
Géry Debongnie 3a461e1dd1 [IMP] qweb: add support for t-tag directive
Very useful in some cases, when one needs to define a generic component.

closes #721
2021-06-04 14:11:31 +02:00
Géry Debongnie e8387810e6 [REL] v1.2.6
# v1.2.6

## Fixes

- router: add support for arbitrary param placements in paths
2021-05-19 12:28:13 +02:00
Samuel Degueldre acac9d1741 [FIX] router: add support for more arbitrary param placements in paths
Before this commit, params in paths had to be be between slashes and
comprise the entirety of the contents between those slashes (eg:
`/books/{{id}}/{{name}}`)

This is unnecessarily restrictive. This commit removes this restriction,
which allows for paths such as:
- `/books/{{id}}-{{name}}`
- `#books&id={{id}}&name={{name}}`

among others

closes #858
2021-05-19 12:25:58 +02:00
Géry Debongnie 968a5460bb [REL] v1.2.5
# v1.2.5

## Fixes
- qweb: correct order for component class lookup (most specific to least specific)
2021-05-18 10:01:22 +02:00
Samuel Degueldre e646eb697e [FIX] qweb: fix order of lookup in t-component directive
Previously, the order of resolution was:
- components defined on the class of the current component
- components defined on the QWeb instance
- the current instance's context

This is unnatural because it doesn't go from most specific to least, as
the current instance's context is the most specific. This is also
fragile, as adding components to the QWeb instance can break unrelated
components.

This commit fixes that by making the lookup start with the current
component's context instead
2021-05-18 09:58:30 +02:00
Simon Genin (ges) 7dfa901332 [IMP] tooling: improve release script
Release script errase dist folder
closes #793

Release can only be done on master branch
closes #829

Update the playground owl version on release
closes #814

Add logError method in release to show messages in red.

Check-formatting command added to the release process.
2021-03-30 10:58:25 +02:00
Géry Debongnie 0290f63ba3 [FIX] component: various issues while mounting manually components
The initial problem solved by this commit is that it was possible to get
into a situation where a mounting/rendering was started, then the component was
updated, but then another mounting operation begins, and it tries to
reuse the previous rendering operation, which is no longer uptodate.

The underlying issue is that Owl did not track properly the various
internal state change of a component.  These issue should be solved by
the introduction of the status enum, which currently tracks 6 possible
states:

- CREATED
- WILLSTARTED
- RENDERED
- MOUNTED
- UNMOUNTED
- DESTROYED

This status number replaces the isMounted and isDestroyed boolean flags.
It has the advantage of making sure that the component is in a
consistent state (it is no longer possible to be destroyed and mounted,
for example)

Another advantage is that it gives us an easy way to track the fact that
a component has been rendered, but is not in the DOM.  This is a subtle
situation where some various events can happen, and we need to be able
to react to that case.

Note that there is a change of behaviour: if a component is mounted in a
specific target, then before the mounting is complete, the component is
mounted in another target, we no longer reject the first mounting
operation.
2021-02-08 10:59:28 +01:00
Géry Debongnie 370fae4e1a [FIX] package.json: rollback live-server from 1.2.3 to 1.2.1
No idea why, but it looks like the npm package 1.2.3 disappeared.
2021-02-04 09:07:26 +01:00
Géry Debongnie 76735410f8 [FIX] qweb: prevent issue with builtin object properties
The QWeb expression parser use an object as a mapping between some
strings and the desired output in the compiled template.  However, as we
should all know, objects are not Maps, they have some additional
properties, such as "constructor" or "hasOwnProperty".

The simple solution is to make sure the mapping object does not have
anything in its prototype chain to pollute its purpose.

closes #835
2021-02-04 09:07:26 +01:00
Géry Debongnie 19a47a7001 [IMP] component: add setup lifecycle hook 2021-02-03 15:32:38 +01:00
Géry Debongnie 398f9f4e53 [IMP] component: can trigger event handlers even if unmounted
From the beginning, Owl was designed to only call event handler on
components that are mounted in the DOM.  The main benefit is that if a
component is destroyed, we are guaranteed to not execute any useless (or
potentially dangerous code).

However, there is one downside: if a component tree is being mounted,
and a child component trigger an event in its mounted hook, then it
cannot be caught by the parent, since the parent is technically not yet
mounted.

This may not be a good situation, but the point is that Owl
unnecessarily prevent the handler to be called.

We can fix this issue by simply checking if the component is not destroyed
instead of checking that it is mounted.

closes #809
2021-02-03 14:46:18 +01:00
Géry Debongnie af426aa902 [DOC] misc small fixes
closes #827, #825, #822, #821
2021-02-03 13:44:04 +01:00
Géry Debongnie 490cf18079 [REL] v1.2.3
# v1.2.3

This is just another small bug fix release, because we need it in Odoo.

Fix: prevent crash when rendering a component before mounting it
2021-01-19 15:39:26 +01:00
Géry Debongnie dfc782599b [FIX] crash when rendering component before mounting
This commit makes sure that Owl does not crash when a component is
created, then updated (for example, with a (observed) state change), and
then, some moment later, mounted.

The initial render is not useful, because it is not linked to a mounting
action anyway.  And it caused issues such as a crash when Owl tried to
patch it to a non existing target
2021-01-19 15:26:15 +01:00
Géry Debongnie 71f545058b [REL] v1.2.2
# v1.2.2

This is just a small bug fix release, because we need it in Odoo.

Bug fix: allow renderings for detached components. Before this release, Owl
ignored renderings in some cases if a component is detached.  We now still render
it, because it is important in some cases.
2021-01-14 09:10:35 +01:00
Géry Debongnie 1a20cc57de [FIX] component can be updated while detached from the main DOM
This commit tries to improve the interactions involving unmounted
components, or components mounted in an htmelement which is detached
from the main DOM, and rendering actions.

The main example is mounting a component in detached div, to prepare all
children.  If we just mount the component, it will work as expected: the
full component tree is rendered in memory, and ready to be really
mounted at the desired target.

However, if before doing that, we update the component and call render
on it (for example, with a change in an observed state), then this
rendering will be ignored, and therefore, the full subcomponent tree is
not uptodate.

This commit will also solve another issue in the compatibility layer in
odoo: in the form renderer, we mount components with the adapter in a
div, which is not yet attached to the DOM. We then manually call the
mounted hook when on_attach_callback is called.  This means that before
this commit, any change to the components between the initial rendering
and the call to mounted will be ignored.

As a bonus, this commit has the effect of bringing closer the semantics
of render and mount operations, which is certainly good.

closes #823
2021-01-14 09:05:05 +01:00
Géry Debongnie 25738a1bf0 [REL] v1.2.1
# Owl v1.2.1

## Changes

- fix: issue with components with shouldUpdate and remounting not rendering
- fix: issue with connected components using onUpdate callback not rendering
- fix: error in todoapp example code
2021-01-08 15:26:28 +01:00
Géry Debongnie 4a96eff3c6 [FIX] store: properly call onUpdate functions in some cases
Before this commit, the following scenario could happen:

Suppose that we have a parent component A, connected to a store,
 and a child component B, also connected to the store and using
the onUpdate feature.

Then, we remount the A component in some other places and a
rendering is initiated in A.  We immediately update the store state.
What happens next is:

- rendering A is done, A internal revid is updated
- store update A (but nothing is done because the state change here
  does not modify A)
- rendering B is done (from parent), B internal revid is updated
- store update B, notice internal revid is updated, does not call the
  onUpdate function

We then have the B component which has not its internal state updated,
because we did not call its onUpdate function.

The solution is to move the onUpdate call in a "preupdate" event, to be
sure that it is called everytime the store is updated.

closes #816
2021-01-08 15:05:07 +01:00
Géry Debongnie d043d47754 [FIX] component: propagate correct info when reusing fibers
In some cases, a rendering initiated in some component is then remapped
into a larger rendering initiated by some parent.

If we have some components which implement shouldUpdate to return false,
then the following scenario can happen:

- some parent component is mounted (which triggers a rendering with
force: true => bypass the shouldUpdate)
- some sub component is updated and rerendered, AFTER the previous
rendering goes through it
- the sub component notices that there is an ongoing rendering, and
  remaps itself in the parent rendering

Before this commit, the new fiber in the subcomponent does not have
force flag set to true, so the new rendering for the subcomponent does
not go through its own children (if they have shouldUpdate=false)

Another more complex kind of scenaria can happen when a remapped
rendering happen with sub components with dynamic shouldUpdate. The
problem is the same at the end: the new rendering should ignore the
shouldUpdate, to make sure we have the last correct information.

With this commit, we make sure that the flag of the new fiber is set to
true.

closes #818
2021-01-07 10:08:29 +01:00
Vishnu Vanneri 3a10468f7b Update todoapp mount issue (#817)
APP gets error while running "ReferenceError: mount is not defined"
because of mount not defined with "Component"
2021-01-06 08:43:33 +01:00
Géry Debongnie 144b323d2b [REL] v1.2.0
# Owl v1.2.0

## Changes

- translation fix for terms surrounded by spaces
- fix: properly remount components with shouldUpdate=false
- add: add two new generic hooks:  and
- fix: do not skip rendering in components using store and local state (in some cases)

Note that the last change is a pretty significant change: component connected to
a store should be very careful if the data that they represent is deleted, because
they will always be rendered with the current state of the store.
2020-12-14 13:23:46 +01:00
Géry Debongnie bb64e87634 [FIX] store: properly render, even if shouldupdate is implemented
Before this commit, an unwanted behaviour happened when using components
with shouldUpdate implemented, and store/state.

The actual problem is the following: the sub component is using a store,
and shouldupdate.  Whenever the component is rendered, it checks if there is an incoming
rendering from the context.  If that is the case, it skips the
rendering, because we actually only want to be rendered by the store
rendering (otherwise, we may run into issue with inconsistent data (more
recent data from the context, older data in the component).

However, if shouldUpdate is implemented, then the rendering coming from
the context simply does not arrive.

To fix this, we tried to just force these renderings to go through all children,
regardless of their shouldUpdate status. This actually works, but then
shouldUpdate is ignored, which is an issue in Odoo discuss.

Then, after discussing this situation, we noticed that the context/store
system is actually unsafe: the protection given by the check mentioned
above is in fact fundamentally insufficient: there are other perfectly
valid situations where a rendering can be triggered on the component,
which will bypass the check (for example, an explicit call to
this.render() or a rendering initiated by some parent component) and
cause a crash if the component is not properly defensively written.

Therefore, it is currently mandatory for all components using
context/store to be aware of that, and to protect themselves against
such situations. So, in that regard, the check is not really a
protection, it just helps hiding an unsafe situation anyway and we
decided to remove it.

Note that this is a potentially breaking change: components using a
store and some local state will now be rendered twice in some cases...

closes #799
2020-12-14 13:19:36 +01:00
Géry Debongnie 9a87b9a4a0 [IMP] hooks: add some building blocks for hooks
This commit introduces two new hooks: useComponent, and
useEnv.
2020-12-14 11:53:01 +01:00
Géry Debongnie 2a53a9592e [FIX] component: force mounting subcomponents with shouldUpdate
Components can implement shouldUpdate to return false.  In that case,
renderings coming from above should be ignored.

However, if the component was unmounted and is remounted, we actually
need to force a rerendering in that case, so it is mounted, otherwise
the subcomponent is left in unmounted state, which means that rendering
are ignored.

closes #800
2020-12-11 15:19:37 +01:00
Ivan Yelizariev f54b9a4a0c [FIX] properly translate terms surrounded by spaces
This reimplements logic from Odoo v13 [1], i.e. if we have a text that follows
by space e.g. "This database will expire in " [2], then we want get translation
for the term without that space and add the space manually. This way we can
export trimmed terms for translations and don't miss the space, when atranslator
forget to add it at the end of translation

[1] https://github.com/odoo/odoo/blob/26927417e2957acba6fb79446c2520107daa7eea/addons/web/static/src/js/core/qweb.js#L46
[2] https://github.com/odoo/enterprise/blob/ab0e893493f909ec3033e8a1cf6c141ea9375587/web_enterprise/static/src/xml/base.xml#L21

---

opw-2410708
2020-12-03 13:36:17 +01:00
Géry Debongnie abb9d0b364 [REL] v1.1.1
# Owl v1.1.1

## Changes

- export  object to make utility functions testable
- minor doc improvements and corrections
- fix: make sure owl does not crash in iframe in private mode
2020-11-20 11:02:19 +01:00
Géry Debongnie 8483cc805e [FIX/IMP] export browser object, prevent crash in private mode
This commit performs two tasks:

1. it exports the internal browser object (in owl.browser), for ease of
testing
2. it makes sure that Owl does not crash inside iframes in incognito
mode, because window.localStorage raises an exception in that case

closes #791
2020-11-20 10:55:08 +01:00
Simon Genin 142f47ac82 [DOC] Fix animation documentation (#783)
The documentation was a bit missleading around which and when css
classes were added to the DOM.
Also added a scss mixin usage exemple.

Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-11-18 09:41:50 +01:00
Stephen Mugisha c8a27aa1a3 Fix minor grammatical errors
Correct minor grammatical mistakes in the `why_owl.md` file
2020-11-02 16:36:16 +01:00
Simon Genin 323cb61d2c [DOC] minor fix - missing this.
Proposed by @obayit with https://github.com/odoo/owl/pull/689
2020-11-02 16:33:02 +01:00
glovebx ac9cc91701 typo correct 2020-11-02 16:03:10 +01:00
Géry Debongnie d83cfc12ee [IMP] qweb: the t-slot directive is now dynamic
It is useful for some kind of components to be able to use a completely
dynamic slot expression.
2020-10-30 15:31:03 +01:00
Géry Debongnie cb07c99d40 [IMP] owl: add a new mount method 2020-10-30 15:31:03 +01:00
Géry Debongnie d615ffd81b [REL] v1.0.13
# Owl v1.0.13

## Bug fixes

- fix: slots: prevent infinite loop in some cases
2020-10-26 08:32:05 +01:00
Géry Debongnie 8d2b250fef [FIX] slots: prevent infinite loop in some cases
The reviewer for the commit on named slots inside named slots did not
notice that there was an infinite loop.  Because of his sloppiness, Owl
could block in an infinite loop when a named t-slots was defined inside
a subcomponent, but not as a direct child.
2020-10-23 12:14:46 +02:00
aab-odoo 7626cc01b3 [IMP] rollup: properly indent generated files (#774)
Also lint rollup.config.js
2020-10-22 13:58:10 +02:00
Géry Debongnie 392185ab67 [REL] v1.0.12
# Owl v1.0.12

## Bug fixes

- fix: properly handle named t-slots inside named t-slots
- fix: do not render comments as text in t-raw
- fix: allow dynamic template in t-call
- fix: correctly handle top-level t-call with body
- fix: correct cause of non determinism in tests

## Tooling improvements

- setup github actions for CI
- improve owl bundling pipeline
- add  flag in owl configuration
- add prettier to github CI
2020-10-20 17:35:23 +02:00
Géry Debongnie 392dc0131a [FIX] config: make enableTransitions work on components (#773)
Fun: the transition is handled at two different places, once for dom
nodes, once for components.  Obviously, I only applied the change to the
first case and forgot about the second.
2020-10-20 17:30:08 +02:00
Simon Genin 4e8e9dee3e Update deploy.yml => add prettier (#772)
* Update deploy.yml

Add the npm run prettier command action to every push request

* Update deploy.yml
2020-10-20 16:38:14 +02:00
Simon Genin 25d4cb2aab Improve the release script for easier deployment (#771)
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-10-20 16:36:14 +02:00
Bruno Boi 4724652533 [IMP] component: allow using TypeScript type hints (#758) 2020-10-20 16:30:09 +02:00
Géry Debongnie b08cc1d084 [IMP] config: add enableTransitions flag to config
It is useful to be able to disable the t-transition directive for
testing purposes.  This commit introduces a global config flag to do
just that.

closes #768
2020-10-20 13:22:25 +02:00
Géry Debongnie a68d7774c6 [FIX] qweb: issue when rendering twice dynamic t-calls
The previous implementation generated an id each time the template was
rendered, instead of just once per new dynamic template found.
2020-10-20 08:02:03 +02:00
Géry Debongnie ed9d820d9e [FIX] tests: fix cause of non determinism
Since commit 7e4baf6, some tests were failing non deterministically. It
seems like the root cause for the problem is that the nextTick method
was sometimes completed too early.

From my understanding of the problem, the nextTick method was correct,
however it is running in a jest test, in node, which simulates the
requestanimationframe behaviour, probably not perfectly.

Switching to an alternate implementation, which swaps the setTimeout and
the requestanimationframe seems to be more solid.

Note that this commit had to introduce the nextFrame method as well,
because the animation tests really seems to want to be in the next
frame, not in a callback executed in the same frame, but at the end.

closes #757
2020-10-19 21:43:49 +02:00
Géry Debongnie 4a2d019161 [DOC] qweb: explain how to work with iterables in t-foreach
Owl is meant to stay low level-ish, and making it work magically with
sets/maps/... seems to be a little too much in term of code/magic. Also,
using iterables in t-foreach is actually quite simple, making this
improvement not as important.

closes #720
closes #754
2020-10-19 20:52:31 +02:00
Géry Debongnie 89eae191b1 [FIX] qweb: correctly handle top level t-call and body
Whenever a top level t-call was made with some non empty body, Owl
complained that a template should not have more than one root node.

The reason was that the compilation context for the body of the t-call
directive was the same as the root compilation context, and it already
had a parentNode set.

To fix the issue, this commit simply use the subContext method to create
a different compilation context, which actually makes sense, because the
body of a t-call is really a different situation.  Also, as a bonus, it
slightly improves the code for the t-call directive.

closes #760
2020-10-19 20:51:44 +02:00
Géry Debongnie 3fbc02986c [FIX] qweb: allow string interpolation in t-call
This commit allows the t-call directive to choose a dynamic template.
This is working in QWeb Python, but was not possible in Owl.

closes #709
2020-10-19 16:27:38 +02:00
Géry Debongnie a28ce440dc [REF] qweb: simplify _compile method arguments
Strangely, the _compile method required a CompilationContext whenever it
was compiled as a sub template, but this parent context was actually not
really needed.  I guess that it was the case in the past, but this was
changed at some point.

This commit makes another significant change: the xml element is no
longer mandatory.  It is actually only required for slots (because the
template is not registered to qweb).

Finally, the interface for the whole method has been changed to use an
option object, which makes more sense with 3 optional paremeters.
2020-10-19 16:27:38 +02:00
Géry Debongnie 8e018fbbda [FIX] qweb: do not render comments as text in t-raw
The code converting html to a vnode did not handle comment nodes as
expected.

closes #761
2020-10-19 16:27:14 +02:00
Simon Genin 33f7fc8e83 [IMP] bundling: Improve owl bundling process and output (#751)
The bundle pipeline can use a bit of a improvement:

Most of the process is moved from npm scripts to rollup.
Add package keys to smooth out the use of bundlers of the end users.
(Rollup by example check amongst others the main, module and browser key
to import the right version of a lib when needed).
Refactor rollup logic to make it more modular.
Add bundled module formats.
Add comments to tsconfig to see more clearly what's possible.
Add reference tag to help @types discovery in test files.

Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-10-16 16:11:11 +02:00
Simon Genin 6a833c54ec [IMP] setup github actions for CI
Add action to launch the tests on the PRs
2020-10-13 16:57:24 +02:00
Géry Debongnie cb38d795f9 [FIX] slots: properly handle named t-slots inside named t-slots
Previous code naively handled nested t-set-slots: if a second named
slots was found, it overrode the first.

In this commit, we use a set to make sure that we only use the first
found t-set-slot node. Also, we ignore set-slots defined in a sub
components, because these slots are only relevant to the sub component
itself.

Note that it works as expected because document.querySelectorAll
performs a search depth first, so we will always use the named slots
closer to the parent element, in term of depth.

closes #682
2020-10-09 15:01:25 +02:00
Géry Debongnie 8da68e925b [REL] v1.0.11
# Owl v1.0.11

- improve release script to make sure we get correct code on npm
2020-10-08 10:50:39 +02:00
Géry Debongnie 474ffa1cd9 [FIX] release: try to publish correct version on npm 2020-10-08 10:48:08 +02:00
Géry Debongnie e73fb462c5 [REL] v1.0.10
# v1.0.10

Bug fixes!

- qweb fix: scoping issue with t-call in t-foreach
- qweb fix: issue with t-set with a body in a t-call
- qweb fix: handle input value attribute as a property
- qweb fix: allow t-call on arbitrary nodes
- qweb fix: add indeterminate to special input properties
- component fix: allow using t-model with bracketed expression
- component fix: properly validate multiple props
- component fix: issue with higher order component, and t-keys
- component fix: issue with unmounted children that should be destroyed
- component fix: make concurrent renderings more robust in some cases
- component fix: allow using vars with body as props
- observer fix: do not proxify promises
- test infrastructure: stop mocking requestanimation frame
2020-09-18 15:16:27 +02:00
Géry Debongnie 38c7ad9629 [FIX] t-model: support expressions with [ ]
Before this commit, the t-model directive worked well with expressions
such as "state.value", but not with bracketed expression: "state[value]"
(it generated invalid code).

This commit make the t-model smarter by detecting this case, and
properly capturing the base expression and key variable.

closes #694
2020-09-18 14:58:48 +02:00
Géry Debongnie d0c76c5854 [IMP] qweb: add indeterminate to special input properties
Input with type="checkbox" have a special property (indeterminate) to
visually display the fact that the input value is non determinate (in my
chrome browser, the checkbox is then drawn with a simple - inside). It
does not actually modify the value of the input, only the way it is
displayed.

So, with this commit, owl will properly set the property, as expected.

closes #713
2020-09-18 13:43:25 +02:00
Géry Debongnie e032314739 [FIX] qweb: allow t-call on arbitrary html nodes
This is a rarely (if ever) used feature, but according to our qweb
reference implementation, it is possible to use the
t-call directive on an arbitrary html tag, like this:

<div t-call="my.template"/>

It is then interpreted as:

<div><t t-call="my.template"/></div>

So, with this commit, we make sure that the owl qweb implementation
matches that behaviour.

closes #706
2020-09-18 08:39:57 +02:00
Géry Debongnie b2db7f21ed [FIX] observer: does not proxify promises
Promises are kind of special, and do not behave like usual javascript
values.

For this issue, the problem is that when the observer tries to observe a
promise value, the code will crash with an error like this:

Uncaught TypeError: Method Promise.prototype.then called on incompatible receiver [object Object]

Also, note that it does not make much sense to proxify promise methods
anyway, since they are not (supposed) to be modified.

So, with this commit, we simply consider that promises should be treated
like a primitive value: simply ignored when determining if it should be
proxified

Note that I actually believe that putting promises in a useState is not
a good idea in general.

closes #677
2020-09-17 13:51:10 +02:00
Géry Debongnie 7b8ac13d3f [REF] qweb: mostly revert fix with t-call and vars with body
The previous fix (overriding tostring of VDomArray) is actually more
general, and solves the same issue. So, let us simplify the code and
keep the more general solution.

This reverts commit 3bf91afc3f.
2020-09-17 09:44:08 +02:00
Géry Debongnie 06d852fcf9 [FIX] component: allow using vars with body as props
Consider this scenario:

- a variable v (with a body) is defined in a template
- it is then passed to a sub component as a prop
- and now, it is t-esc-ed.

Before this commit, the displayed value was [object object], because the
value actually passed to the sub component was a VDomArray (internal
structure used to represent nodelists)

This issue is actually quite a problem in practice, because values in a
templates are translated, but not in attributes.  Therefore, using a
t-set directive with a body text content is the proper way to have
translated values at runtime.

We override in this commit the method toString of VDomArray to make sure
it is properly displayed.

Note that we considered changing the way props were generated (by trying
to detect VDomArray, then calling vDomToString), but then the value
would not be able to be used in a t-raw.  Also, it is quite elegant to
be able to format the VDomArray only at the end.

closes #670
2020-09-17 09:44:08 +02:00
Géry Debongnie 7e4baf668a [FIX] test: timing issue
Before this commit, we artificially replaced in the tests the
requestAnimationFrame by a setTimeout, to actually increase the speed of
the tests.  However, this is not really a true replacement.  For
example, a real setTimeout can come before or after a real
nextAnimationFrame, depending on when/where it is requested.

Also, this change exposed another problem: the nextTick function did a
setTimeout before a nextanimationframe. This is not a problem when
nextAnimationFrame is replaced by a setTimeout, because then all
expectations holds in owl.  However, it is wrong: to get to the next
animation frame, we need to request an animation frame, and THEN wait
with a setTimeout.

closes #729
2020-09-17 09:18:05 +02:00
Géry Debongnie fe34ba00a6 [FIX] component: properly validate multiple props
Because of a "break" statement instead of "continue", the check for valid
props was stopping as soon as it met an optional props, which kind of
invalidate the whole system.

closes #717
2020-09-17 08:33:18 +02:00
Géry Debongnie e5e7790530 [FIX] qweb: handle input value attribute as a property
Sometimes, HTML is slightly more subtle than what I initially expect.
Rendering some html is simple, we have tags and attributes.  However,
once we add behaviour, then the situation is more complex:

<input value="abc"/>

is an input with an INITIAL value of "abc", but the attribute does not
actually represent the CURRENT value of the input, which may be
different if the user did change it.

This is basically the difference between "attribute" and "property".

So, when rendering html with owl, we sometimes want to actually set
the property (current value), instead of the html attribute.

This commit make sure that this is the case for inputs with the "value"
attribute.

closes #722
2020-09-17 08:32:02 +02:00
Géry Debongnie 3bf91afc3f [FIX] qweb: fix issue variables set in body of t-call
The body of a t-call directive may be used to define private variables
to the sub template call.

However, the code that handles t-call worked like this:

- compile sub template if necessary
- then compile body of t-call to extract variables

This means that the variables defined in the t-call body were not yet
processed and available in the context.  Because of that, when the call
to t-esc is done, there is not internal qweb var, and the code simply
outputs a scope['varname'], which is in our case a VDOMArray, so it is
displayed as [object object]

What this fix does is changing the way t-esc works: if we are in the
context of a sub template, then it assumes that any outside variable may
or may not be a VDomArray, so it needs to check and eventually convert
it to a string, if necessary.

closes #719
2020-09-16 11:39:32 +02:00
Géry Debongnie 2529aa3ef2 [FIX] component: make concurrent renderings more robust
Here is a situation that can happen in some complicated case:

1. a parent component is rendered, which includes some children
2. it is then willPatched
3. the sub components are then mounted/willUnmounted
4. because of complicated business logic, this causes the parent
component to be rerendered (before parent "patched" method is called)
5. owl will internally reset its currentfiber to null (but there is a
pending rendering!)
6. subsequent rendering will ignore pending rendering
7. havoc ensues

This is actually one of the reason why modifying a component state in a
willPatch component is actually not a good idea.  However, the good news
is that this specific situation can be properly handled: we can simply
make sure that we do not reset currentFiber to null if there is a new
pending rendering.

closes #728
2020-09-16 09:21:33 +02:00
Géry Debongnie 8d25bddda4 [FIX] component: subtle issue with unmounted children
Owl has to manage a lot of interesting situations.  One of them is when
a rendering is initiated, which creates a sub component, but then
another rendering starts, which invalidate the previous one, and will
create another sub component.  Since the first sub component was not
ever in the DOM, we cannot rely on the vdom patching process to remove
it, so we have to do it manually.

Sadly, this is actually a very tricky situation, since there are other
subtle situations where the code that remove an unmounted widget could
be executed, in particular when the parent component is unmounted, then
remounted, then modified to trigger yet another rendering.

In this commit, we handle this case more carefully by making sure that
the destroyed subcomponent properly configures its pvnode so the patch
process happens as expected.

joint work with the framework team, and in particular LPE for his work on
finding a testcase!

closes #724, #731
2020-09-15 15:51:30 +02:00
Lucas Perais (lpe) 81af21a025 [FIX] component, fiber: update props with virtual node should not crash
Have a hierarchy of A, B, C components where:

```xml
<div t-name="A">
  <div>
    <B t-key="key1"/>
  </div>
</div>

<t t-name="B">
  <C t-key="key2"/>
</t>

<div t-name="C">
  <div><t t-esc="keys_as_props" /></div>
</div>
```

The subtility of the issues lies in B, which doesn't have its own
concrete DOM element, rather, it borrows it from C.

With the sequence of events:
- change key2
C1 is destroyed and replaced by another instance, and another node.
B1 has its props updated and is patched with the C2's node (CRITICAL)
A1 is patched

- change key1 AND key2
C2 is destroyed
B1 is destroyed
A1 is patched replacing B1 by B2, and their nodes too (which at this point should be C2's to C3's)

Before this commit, at the CRITICAL point, the node representing the component itself
(technically its pvnode) was not updated with the new concrete node provided by B1 patch with C2 node
i.e. it held the previous node still
The second array of steps crashed because at A1 patch, the new B2 node would replace B1, which
was out of the DOM (removed because C1 was destroyed long before),
and therefore without a viable parent to insert B2 node.

After this commit, we update the component's pvnode after the patch which elm had possibly changed
There is no crash anymore for this use case.
2020-09-14 15:17:45 +02:00
Jigar Patel 9baea2c1cd [DOC] props validation: The validation type should be a String object. 2020-07-28 08:56:33 +02:00
Lucas Perais (lpe) 8e03f9cd9c [FIX] qweb: t-call should protect scope and let it accessible
Have a t-call nested in a t-foreach nested in a t-foreach

```xml
<t t-name="template">
  <t t-foreach="..." t-as="a">
    <t t-foreach="..." t-as="b">
      <t-call="templateCalled" />
    </t>
  </t>
</t>
```

Before this commit, the `a` variable was not accessible within the t-call.
That was because the way t-call protected its scope by hiding other protected scope
in this case, the first protected scope for the first `t-foreach` was hidden

After this commit, `a` and `b` are accessible in the t-call, whether the t-call
defines its own variables by `t-set` or not.
Also, as expected from other fixes, there is no leaks of variables defined within a `t-call`

fixes #695
2020-07-28 08:56:00 +02:00
Géry Debongnie c5a2f52afb [REL] v1.0.9
# v1.0.9

- qweb fix: add support for svg namespace in t-raw
- qweb fix: properly handle subtemplates in shared templates
2020-06-02 10:38:32 +02:00
Michael Mattiello (mcm) 9fd8315c53 [FIX] qweb: support of svg in t-raw
Before this commit, t-raw a string containing `<svg>` did not display
the svg element. This was because of missing namespace in vnode's data.

Now, `vdom.addNS` is called to add the svg namespace when necessary.
2020-05-25 14:02:34 +02:00
Géry Debongnie 85318b3ae6 [FIX] qweb: properly handle subtemplates in shared templates
With QWeb, we can register globally templates (using the xml tag or
the registerTemplate function). However, these templates, once
compiled, can generate sub template compiled functions. Before this
commit, these sub functions were local to a specific instance.

This means that creating a new QWeb instance and rendering a global
parent template would crash, since it was unable to find the actual sub
function.

This commit fixes the issue: the sub functions are now shared
statically, but with a unique ID, so we do not have issues with sub
functions having a same name in different QWeb instance.

closes #701
2020-05-25 11:43:24 +02:00
Géry Debongnie 4b961cbffe [REL] v1.0.8
# v1.0.8

- qweb fix: do not override t-att-class with class attribute in some cases
- component fix: refuse to mount a destroyed component
- hooks type improvement: useRef is now generic, and can capture the type of a component
- qweb fix: t-esc inside t-call using outside t-set
2020-05-18 09:07:31 +02:00
Aaron Bohy 2af8cccd65 [FIX] qweb: t-esc inside t-call using outside t-set
Before this commit, the generated code was incorrect, and crashed,
when there was a t-esc="abc" in a subtemplate, with t-set="abc"
done outside the subtemplate, with syntax <t t-set="abc">value</t>.
2020-05-15 13:47:57 +02:00
Géry Debongnie b8d3618afa [IMP] hooks: improve useRef typings
With this commit, we can get the type for the target component for a
ref.

By default, if the generic type is not given, the ref will simply use
the base Component type.
2020-04-30 16:23:36 +02:00
Géry Debongnie 559fadb62a [FIX] component: properly handle mounting a destroyed component
part of #685
2020-04-22 09:29:39 +02:00
Géry Debongnie c36333dbbc [CLEANUP] update prettier to v2.0.4 2020-04-21 16:23:31 +02:00
Géry Debongnie b5c3422b4d [FIX] qweb: do not override t-att-class with class
There was some code in qweb to make sure that we support setting class
and t-att-class on the same html element:

<div class="some class" t-att-class="{b: true}">...</div>

But the code did not work in the other direction:

<div t-att-class="{b: true}" class="some class">...</div>

With this commit, we just add the missing if statement

closes #664
2020-04-21 16:23:31 +02:00
Géry Debongnie 23ce19e57a [REL] v1.0.7 2020-04-17 15:52:52 +02:00
Géry Debongnie aaf40e9bec [IMP] tooling: add iife build to npm package
This is useful to link to the iife version in some other projects.
2020-04-17 15:47:20 +02:00
Géry Debongnie 211f6ebdd6 [REL] v1.0.6 2020-04-17 15:16:24 +02:00
Géry Debongnie 1707bd240d [IMP] add browser bindings to standard environment
This could be done by each application, but it does cost only a few
lines of code, and it helps standardizing the Owl ecosystem.

For example, some library (such as o_spreadsheet) needs to mock side
effects, and Odoo also needs to do that, so this prevents duplicated effort.

closes #686
2020-04-17 13:39:46 +02:00
Géry Debongnie 142b69823f [IMP] types: do not make Env an indexed type
Before this commit, Env was an indexed type, this means that one could
write env.anything, and it would accept it as a valid type. This is
actually quite dangerous, because we lose the typing advantages for all
keys that are properly defined.

For example, if a component is defined as:

class MyComponent extends Component<Props> {
 ...
}

Then Typescript will let it use anything from the environment, even if
it is wrong.  So, most properly typed Typescript applications should use
instead a sub environment:

interface MyAppEnv extends Env {
  someKey: someValue
}

Then, the component should be defined this way:

class MyComponent extends Component<Props, MyAppEnv> {
 ...
}

Before this commit, any typos in the environment accesses would not be
noticed by typescript.
2020-04-17 13:39:46 +02:00
Joseph Caburnay 94c8bce810 [IMP] allow anonymous Component extensions
It is possible that a Component is extended dynamically and if this is
the case, the class that extends it can be anonymous, with property
name=''. If this is the case, current implementation interprets the empty
string to be false so the while loop is terminated without further
scanning the super classes.

In this proposal, we allow anonymous class to be scanned until its
Component ancestor. Basically, the anonymous class assumes the name of
it super.
2020-04-15 08:54:02 +02:00
Géry Debongnie ae172d42e7 [IMP] slots: add new t-set-slot directive
This new t-set-slot directive is meant to replace t-set when we need to
define the content of a sub slot. All new code should use that
directive.

The old t-set directive is still supported for now, but this should be
removed when we publish Owl 2.0.
2020-04-06 15:42:32 +02:00
Géry Debongnie ddf30a8a97 [CLEANUP] run prettier on the codebase 2020-04-06 15:42:32 +02:00
Géry Debongnie 8d0d8538ad [FIX] slots: better heuristic to determine named slot content
Unfortunately, we chose to use the directive `t-set` to define sub slot
contents in a template.

The goal was to reuse a directive for a similar use case: defining sub
template is almost the same as defining a slot content.

Obviously, this introduces a name conflict: the inner content of a
component cannot use t-set t-value anymore (nor t-set with a body
value), since they are interpreted as slot names.

We mitigate the issue here by only interpreting as slot content the
`t-set` statement located immediately below the parent component tag
name and with a body content.

However, a real fix need to introduce an additional directive to resolve
the ambiguity.
2020-04-06 15:42:32 +02:00
Joseph Caburnay 2b0315c03f [FIX] hooks: order of brackets for multiple async hooks 2020-03-30 08:46:16 +02:00
lucas c469cac315 [FIX] qweb: protect scope in t-call writing recursively
Have something like
```xml
 <div t-name="Parent">
   <t t-call="nodeTemplate">
     <t t-set="recursive_idx" t-value="1"/>
     <t t-set="node" t-value="root"/>
   </t>
 </div>

 <div t-name="nodeTemplate">
   <t t-set="recursive_idx" t-value="recursive_idx + 1"/>
   <p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
   <t t-foreach="node.children or []" t-as="subtree">
     <t t-call="nodeTemplate">
       <t t-set="node" t-value="subtree"/>
     </t>
   </t>
 </div>
```

Where we want to propagate a recursion index through recursive t-calls

Before this commit, it did not work as we protected the scope in order
to not leak, in the wronf manner. Namely the protected scope only took
firt level prototype properties of the original scope.

After this commit, this case works as we mark the scope as read only

solves #672
2020-03-12 13:15:28 +01:00
Lucas Perais (lpe) b4ad14edc0 [FIX] component: self mounting position keeps the reference
Have

```xml
<body t-name="webclient" />
```
and
```js

const comp = new WebClient();
comp.mount(document.body, {position: 'self'});
```

Before this commit, the body that was there before anything had happened
was *replaced* by the new body node created by the WebClient OWL component

After this commit, we ensure that the element body is the same at reference level
2020-03-05 16:35:33 +01:00
Aaron Bohy 718e5264ae [FIX] test: missing await 2020-03-03 10:42:22 +01:00
Aaron Bohy f5d019bb69 [FIX] component: concurrency issue with cancelled fiber
Since ee956a197, the rendering is skipped if the currentFiber is
completed. Unfortunately, cancelled fibers remain set in __owl__,
so when a fiber is cancelled, subsequent calls to render are
skipped.

It would be nice to reset __owl__.currentFiber to null when the
fiber is cancelled, but when trying to do so, a lot of tests fail.

Part of issue #622
Closes #665
2020-03-02 11:35:47 +01:00
Géry Debongnie afa36f52a0 [DOC] doc: fix t-debug example
also, remove a useless console.log

closes #661
2020-02-24 11:05:12 +01:00
Géry Debongnie fd6327b068 [REL] v1.0.5 2020-02-21 09:47:01 +01:00
Géry Debongnie d5098dd138 [MISC] set package.json license field to a valid SPDX expression
closes #646
2020-02-21 09:32:35 +01:00
Géry Debongnie ce052e0992 [FIX] transition: no crash if added/removed quickly
This commit should fix crashes coming from transition code on nodes.
This does not impact transitions on components.

closes #637
closes #641
2020-02-21 09:30:47 +01:00
Géry Debongnie b00188c1ce [IMP] qweb: add support for event capture
In this commit, we uses the "!" as suffix to designate an event that
should be captured.  This is inspired by the Vue source code.

This solves the issue of communicating additional information to the
underlying virtual dom.  However, this will most likely disappear when
we rewrite the vdom as a virtual block system.

closes #650
2020-02-21 09:22:43 +01:00
Géry Debongnie 2c01802b8e [FIX] qweb: support attributes with quotes
closes #651
2020-02-21 08:08:55 +01:00
Géry Debongnie 20eb848262 [FIX] component: merge hooks properly
Before this commit, creating a sub component with
t-att-style/t-att-class attributes or with t-on- event handlers would
override the *hook* object rendered by the sub component.

This is an issue for some directives, such as t-ref, which defines
hook functions.

This commit fixes the issue by checking for an override, and wrapping
the hooks in a function that calls each defined hook.

closes #638
2020-02-17 15:50:53 +01:00
Géry Debongnie 8b479749b7 [IMP] add yarn.lock to .gitignore 2020-02-17 13:06:23 +01:00
Géry Debongnie f7728b93bd [FIX] component: cancel previous mounting operations if necessary
closes #626
2020-02-17 12:49:32 +01:00
Géry Debongnie 03585d8fea [DOC] add dedicated page on error handling
closes #632
2020-02-17 09:11:04 +01:00
Géry Debongnie 047a9c8993 [DOC] add dedicated page on event handling
part of #632
2020-02-17 09:11:04 +01:00
Géry Debongnie 69d35abe4f [DOC] add a dedicated page on slots
closes #640
2020-02-17 09:11:04 +01:00
Géry Debongnie 1d7503913e [FIX] qweb: ignore comment nodes between t-if/t-elif/t-else
closes #636
2020-02-17 08:10:20 +01:00
Géry Debongnie ee956a1977 [FIX] component: resolve a subtle concurrency issue
Asynchronous rendering is subtle. Before this commit, it was possible
(though not easy) to get into a situation where the Owl rendering
pipeline decremented twice the counter of a fiber that indicates that
its work is complete.

The situation occurs in the `render` method, where we create a fiber,
wait for a micro tick before starting the actual rendering (the goal was
to batch all changes coming from a single call stack). However, in this
microtask tick, it was possible that the fiber was cancelled, and we did
not have a check for that.
2020-02-07 14:19:43 +01:00
Géry Debongnie 3fdc7a48f3 [IMP] types: make component generic types optional
In most cases, we just want Component<any, Env>. But since it was so
annoying to have always the type Env, we actually used
Component<any,any> everywhere.

With this commit, the generic types have a default (and their order is
swapped), so we can simply use Component in most cases, and
Component<Props> when we want to type the props.
2020-02-06 09:37:44 +01:00
Fabrice Henrion d212309f1b [FIX] typo
Courtesy of the en_US police
2020-02-04 09:03:02 +01:00
Géry Debongnie 8866905f33 [DOC] large refactoring, add section on starting project 2020-02-03 22:06:44 +01:00
Géry Debongnie 50b116c56d [DOC] add information on why odoo made owl 2020-02-03 22:06:44 +01:00
Géry Debongnie a823373220 [FIX] tool: properly wait in release script 2020-01-24 15:25:08 +01:00
Géry Debongnie e402ee62de [REL] v1.0.4 2020-01-21 10:28:50 +01:00
Géry Debongnie 5711eb065d [ADD] tooling: add release script
More work is needed to complete this, but the basics is here, and it is
difficult to test.

closes #612
2020-01-21 10:10:45 +01:00
Géry Debongnie 6739e79aac [ADD] tools: add single file component example to playground 2020-01-21 09:27:30 +01:00
Géry Debongnie 1c8dc97fca [FIX] component: can mount on different target without unmounting
closes #616
2020-01-21 09:26:38 +01:00
Géry Debongnie f9cac94dc7 [REF] tests: split component.test into multiple files 2020-01-21 09:26:38 +01:00
Géry Debongnie c62c3fea19 [FIX] types: improve type of useExternalListener
so it can work on window

closes #613
2020-01-21 08:11:40 +01:00
Géry Debongnie 3845607feb [DOC] store: clarify how a component can find a store
closes #617
2020-01-21 08:08:14 +01:00
Géry Debongnie 3fcbb9ad16 [REL] v1.0.3 2020-01-09 16:27:18 +01:00
Géry Debongnie e04b160bff [REL] v1.0.2
v1.0.2 because I am fighting with npm...
2020-01-09 15:12:32 +01:00
Géry Debongnie bc234d2053 [ADD] tools: add benchmarks for 1.0.0 2020-01-09 14:52:27 +01:00
Géry Debongnie 3d0f046d9a [REL] v1.0.0
Finally!!
2020-01-09 14:37:36 +01:00
Géry Debongnie 4c75bae0e8 [IMP] owl: improve npm packaging
closes #611
2020-01-09 14:35:13 +01:00
Géry Debongnie 54f5819ef9 [ADD] hooks: add useExternalListener hook
It is very useful.

Also, this commit prettifies the code.

closes #608
2020-01-09 14:06:30 +01:00
Joseph Caburnay 0c9c5b877b [FIX] playground: not all completed is deleted
Before this fix, not all completed tasks are deleted when clicking clear
completed button because we are looping to a mutated array. Looping thru
a copy of the mutated array fixes this problem.
2020-01-09 09:40:13 +01:00
Joseph Caburnay b7b0e38fca [FIX] docs: some spelling corrections 2020-01-09 09:40:13 +01:00
mcm-odoo 74bd119bf5 [IMP] styles: handle & selector
This commit adds the support of the & selector.
This selector is useful to join a subrule with a parent selector.

example:

button {
  &:hover {
    background-color: red;
  }
}

will give

button:hover {
  background-color: red;
}
2020-01-09 09:37:27 +01:00
Géry Debongnie 4a0d04e4ae [FIX] debug tools: do not crash on non json-stringifiable props
closes #595
2020-01-09 09:24:50 +01:00
Géry Debongnie 49931104a1 [CLEANUP] update snapshots 2020-01-08 16:05:22 +01:00
Lucas Perais (lpe) 21d1306ec2 [FIX] qweb: t-foreach needs to hold on scope
Have a t-on within a t-foreach
the t-on has an expression

Before this commit, when triggering the t-on, the expression was falsely evaluated
In details, the expression took scope from outside the foreach loop
as we protect it to have similar results than an actual for loop

But, at triggering time, the scope protection had already been terminated
meaning the info of the iteration of the loop the handler has been built in
had already disappeared

This commit fixes that

closes #594
2020-01-08 15:45:44 +01:00
Lucas Perais (lpe) 21737d33fa [FIX] qweb: variables set outside foreach must be altered by inloop t-tset
In a template, have t-set t-value outside a t-foreach
in the t-foreach, alter that variable by resetting it (as for a incrementation variable)

Before this commit, when printing the variable when the loop had finished
its value was the one set in the first place

After this commit, the value becomes the one altered by the loop iterations

closes #598
2020-01-08 15:45:44 +01:00
Géry Debongnie 23012f3e7c [FIX] component: multiple t-calls
Before this commit, owl could crash in some specific situations:
multiple t-calls with sub components, outside a loop.  The reason is
that the t-call did not generate a key, so from the point of view of the
children component, it had the same key, even though it was at a
different place in a template.

Also, with this fix, we can fix the playground responsive example.

closes #602
2020-01-08 15:24:43 +01:00
Géry Debongnie 55889febd5 [FIX] qweb: add support for "in " operator in expressions
closes #603
2020-01-08 15:20:01 +01:00
mcm-odoo 0af46bc123 [FIX] styles: fix selector generation
This commit fixes the css selector generation for nested rules

Before this fix, a selector like:

.parent {
	.child-a, .child-b {
		color: red;
	}
}

was generated as:

.parent .child-a, .child-b {
	color: red;
}

Now it generates:

.parent .child-a, .parent .child-b {
	color: red;
}
2019-12-27 12:31:17 +01:00
Géry Debongnie b63cd4c9d9 [REL] v1.0.0-beta5 2019-12-20 11:26:53 +01:00
Géry Debongnie 953778dc50 [IMP] component/tags: add inline css tag
This add an important feature: defining completely standalone owl
components, with the template/style and javascript code together.

closes #284
2019-12-20 11:20:10 +01:00
Géry Debongnie 4f61d9f1e0 [FIX] component: no crash if empty props
The expression parser properly format an empty string into an empty
string. But then, this empty string was injected in the props object of
a component, and the compiled code looked like this:

let props5 = { val: };

So, in this case, we simply put undefined, since there are no props
value.

closes #587
2019-12-19 14:32:10 +01:00
Sébastien Theys bc2c7edff4 [FIX] store: prevent crash when selector returns null
Because `typeof null === 'object'`.
2019-12-19 14:24:36 +01:00
Géry Debongnie c8d9c0b50e [DOC] add npm install instruction 2019-12-19 14:12:00 +01:00
Géry Debongnie 15b25fd838 [IMP] owl: prepare owl to be released on npm
closes #196
2019-12-19 14:09:41 +01:00
Géry Debongnie 1db0f5ac9b [REL] v1.0.0-beta4 2019-12-17 15:26:49 +01:00
Géry Debongnie 6465665550 [FIX] qweb/component: handle nested t-foreach and components
The way keys were handled in QWeb was mostly ok, but a little naive. It
is fine when we deal with a list of dom nodes, since the reconciliation
algorithm need only to be able to differentiate/reconcile nodes in that
list, but it is an issue with components, which needs to globally be
able to find themselves in their parent's children map.

Because of the way this was handled, there were situations were Owl
internal virtual dom would crash, since components wrongly assumed that
they were already rendered in a different place.

To fix this, we generalize the way keys are generated, by concatenating
all sub keys coming from each iteration loops.

closes #584
2019-12-17 15:23:24 +01:00
Géry Debongnie 9216c5c24b [FIX] qweb: t-call properly transfer key to sub components
Since t-call is now a function call, we need to properly handle the
internal key used to find previous components.  If a t-call is inside a
t-foreach, then we need to transfer the key to the sub template.
Otherwise, each component in the subtemplate will be associated to the
same key, which means that it will lead to big issues: components are
destroyed and reused...

closes #581
2019-12-16 20:30:34 +01:00
Géry Debongnie d9bf4284c6 [REL] v1.0.0-beta3 2019-12-16 09:56:35 +01:00
Géry Debongnie ef6fd457f8 [FIX] qweb: add support for spread operator
closes #575
2019-12-13 22:35:26 +01:00
Sébastien Theys 455e45c148 [FIX] store: make Array.map work correctly on the result of selector
closes #577
2019-12-13 20:32:02 +01:00
Lucas Perais (lpe) 5e7f1d5e6d [FIX] qweb: t-call must pass extra parent
Before this commit, when template was t-call'ed and defined
t-component directive within it, there was a crash because extra.parent was never defined

After this commit, this use case works
2019-12-13 20:28:49 +01:00
Géry Debongnie 0b05ad619f [FIX] qweb: t-set with a body should not be joined with comma
closes #574
2019-12-13 20:15:37 +01:00
Géry Debongnie 29a80d0b28 [FIX] qweb: properly display falsy values
While it is not tested, nor documented, the reference QWeb
implementation display the `false` value as "false". So, we have to
adapt the Owl implementation to match that behaviour.

At the same time, this commit uses 'let' instead of 'var' in various places,
to make the compiled code more consistant.
2019-12-13 14:48:19 +01:00
Géry Debongnie 0931a4dc5b [FIX] qweb: bind handlers to component
Before this commit, the handlers were bound to the current context,
which is often the component but not necessarily.  In some cases, a sub
scope can be created (with t-foreach, or slots, or t-call), and the
context is actually an object with the actual component instance it its
prototype chain, but not the component.
2019-12-12 14:32:43 +01:00
Géry Debongnie 4e22dbcad6 [FIX] qweb: accept assignations in qweb expressions
closes #560
closes #569
2019-12-12 14:32:43 +01:00
Géry Debongnie b283e65ad4 [REF] qweb: improve t-on generated code
and deduplicate logic between directive t-on applied on a node and on a
component
2019-12-12 14:32:43 +01:00
Géry Debongnie f517d44e32 [IMP] qweb: slightly simplify generated templates
- In some cases, we can simply inline a template key instead of assigning
  it to a variable and then using that variable
- Add component name in generated code for components
2019-12-12 09:39:26 +01:00
Lucas Perais (lpe) 08022b49df [FIX] qweb: conditional t-call with body
When a t-call has a condition and a body
the t-if's and its flavors should be ignored when compiling the body of a t-call
2019-12-11 14:09:45 +01:00
Géry Debongnie b96ea79f2b [FIX] qweb: renderToString now properly escape
closes #565
2019-12-11 12:25:05 +01:00
Géry Debongnie 97d8b3ed8c [REF] tests: remove Widget common class
Bad name, and bad practice to use it. It makes tests less standalone.
2019-12-11 12:25:05 +01:00
Géry Debongnie 9921ae07b1 [REF] tests: move slots tests out of main component file 2019-12-11 12:25:05 +01:00
Géry Debongnie 41cb5f1abd [FIX] *: remove useless values to t-else and t-debug
The actual value does not matter, only the fact that it is present.

closes #564
2019-12-11 12:25:05 +01:00
Géry Debongnie 0762eeb010 [REL] v1.0.0-beta2 2019-12-11 10:19:04 +01:00
Géry Debongnie bc455be720 [IMP] components: slots can now define default content
closes #554
2019-12-11 10:11:47 +01:00
Lucas Perais (lpe) 9145799ae9 [FIX] qweb: evaluate body of t-set immediately 2019-12-11 10:07:41 +01:00
Géry Debongnie ce9c2f8613 [FIX] qweb/component: refactoring of scoping/variables/slots
This commit is a significant refactoring of the internal of QWeb. It
simplifies the way variables/scoping and slots interact together.  The
main idea is that we use a simple scope object instead of a
context/var/scope object.

This commit also implement the actual correct QWeb semantic for the
t-call directive with a sub body.  Before, we simply extracted the
variables from the body and injected them at the top of the sub
template.  We now simply compile the body before the sub template.

This is a joint work with Lucas (lpe).

closes #541
closes #544
closes #545

closes #557
closes #556
2019-12-11 10:07:41 +01:00
Géry Debongnie b890e7ceae [IMP] qweb: add template name in generated compiled code 2019-12-10 16:14:09 +01:00
Géry Debongnie 124211c18b [DOC] clarify store documentation on connected components
closes #538
2019-12-06 12:25:55 +01:00
Géry Debongnie be8c3bbf6f [IMP] tooling: add warning if component not mounted, but rendered
closes #551
2019-12-06 12:07:54 +01:00
Géry Debongnie e162a6fb6c [IMP] qweb: add support for arrow functions in expression parser
closes #542
2019-12-06 11:49:55 +01:00
Géry Debongnie 7ec05ff8cf [IMP] component: add position option to mount method
A component can now be mounted in different positions: either 'before' (at
the start), 'after' (at the end), or 'attach' (take possession of a target
element)

closes #539
2019-12-06 10:53:01 +01:00
Géry Debongnie a0bfbf6dd0 [FIX] qweb/component: do not call handlers for unmounted components
Unmounted components should be considered inactive, at least from the
perspective of Owl itself.

closes #543
2019-12-06 09:58:12 +01:00
Géry Debongnie b67cb71048 [FIX] slots: propagate vars to correct function
closes #529
2019-12-06 09:53:48 +01:00
Géry Debongnie f5b3ef5cf3 [DOC] add some information on variables and t-call
closes #540
2019-12-05 21:49:51 +01:00
Lucas Perais (lpe) ecfccf2448 [FIX] portal: manually unmounting works
Before this commit, when manually unmounting the portal
or one of its parents, the teleported elements were not cleaned
and stayed teleported forever

After this commit, the teleported content is brought back and
unmounted much like any component

closes #531
2019-12-04 09:48:08 +01:00
Géry Debongnie f12d3373c9 [FIX] qweb: properly handle empty class attribute
The classList.add method actually crashes when given an empty string (or
a string with just white spaces).

closes #530
2019-12-03 16:19:09 +01:00
Géry Debongnie 286090efca [FIX] tooling: debug tool logged too many scheduler stops
This was caused by additional calls to flush (because of connected
components)

closes #533
2019-12-03 15:16:29 +01:00
Géry Debongnie 2b4ff7b2fd [FIX] doc: the debugOwl helper was missing a parameter
closes #532
2019-12-03 14:13:42 +01:00
Géry Debongnie c43178a45b [CLEANUP] observer: remove useless variable 2019-12-03 11:17:37 +01:00
Géry Debongnie 033a431e18 [ADD] tools: add benchmark for 1.0.0-beta1, remove old ones 2019-12-03 09:12:52 +01:00
Géry Debongnie 42aa9d3ae1 [REL] v1.0.0-beta1 2019-12-03 08:52:04 +01:00
Géry Debongnie 85f26a0286 [IMP] tools: update debug script, add tests
closes #525
closes #526
2019-12-03 08:28:46 +01:00
Géry Debongnie ed4ab51c17 [FIX] component: tricky concurrency issue
This is a really interesting problem: there was a situation where a
component would not have an event handler properly bound. The reason was
that we were in a situation where the scheduler task queue was flushed
at an unfortunate timing:

- parent component template is rendered
- subcomponent is prepared:
   - sub component is willStarted
   - it is rendered (so, fiber counter is set to 0)
- scheduler task queue is flushed => we patch the DOM
- the code registered in the __prepare.then(handler) is executed, which
  add the createHook to the vnode (but after the dom is patched)

Usually, the last two steps happen in a different order, but in an
environment with connected components, we sometimes flush the task queue
at various moments, so some code could be executed between the end of
__prepare and the registered handler.

The fix is interesting: we give a callback to the __prepare function
instead of registering a .then handler to the deferred.  This callback
is then guaranteed to be called at exactly the proper timing.

Thanks seb for the hard work of finding the root cause of this issue

closes #520
2019-12-03 08:23:10 +01:00
Lucas Perais (lpe) 556a4644f0 [ADD] misc: component Portal
The component Portal is used to teleport the content
in its slot as a child of an element
somewhere else in the DOM, usually 'body'

It is designed to be transparent and is just here to do the teleportation task

OwlEvents (with method `Component.trigger`) are redirected from
any Component instanciated within the Portal (and therefore teleported
somewhere else) to the place it would have been without teleportation
i.e. they are re-triggered onto the Portal root node

Co-authored-by: Aaron Bohy <aab@odoo.com>

closes #184
closes #466
2019-12-02 21:04:07 +01:00
Géry Debongnie 821bd0b4b8 [IMP] tooling: add debug code
closes #521
2019-12-02 09:03:48 +01:00
Géry Debongnie 06a6d890d7 [DOC] move some component doc in sub pages
closes #354
2019-12-02 08:55:12 +01:00
Géry Debongnie c1269288f5 [REL] v1.0.0alpha5 2019-11-29 16:11:34 +01:00
Géry Debongnie 8c17bb0411 [FIX] component: reorganize fiber update algorithm
Closes #473
Closes #484
Closes #489
Closes #492
Closes #512

Co-authored-by: Aaron Bohy <aab@odoo.com>
2019-11-29 15:13:37 +01:00
Lucas Perais (lpe) 7f6782d009 [FIX] qweb: cascading t-call and t-raw="0"
Have a t-call having a t-raw="0"
that call is made to a template of the same form
i.e. itself as a t-call t-raw="0" structure

Before this commit, there was recursion crash
This was because the caller to which a t-raw="0" referred to
was incorrect. The caller here is the node which makes the t-call.
In particular, the caller was always set to last caller of the context

After this commit, there is no crash and this imbrication
of t-call and t-raw="0" is well rendered.
To sum up, we count the recursive calls to t-raw="0" and fetch
the caller in the context accordingly

closes #510
2019-11-29 13:17:40 +01:00
Aaron Bohy 6e5d6aa226 [FIX] slots: use correct scope and vars at update
Part of #473
2019-11-28 14:42:46 +01:00
Aaron Bohy 9400c0adad [FIX] playground: stop using class fields
as Edge doesn't support them.

Closes #509
2019-11-28 10:21:45 +01:00
Aaron Bohy 85d4393242 [FIX] t-slot: correct parented relationship with nested slots
Closes #506
2019-11-28 10:15:57 +01:00
Aaron Bohy 6c8b401092 [FIX] component: slots: multiple slots with components
Closes #508
2019-11-27 15:47:55 +01:00
Aaron Bohy 12be815342 [REF] qweb: use global nextID
and reset it to 1 before each test s.t. snapshots are deterministic.
2019-11-27 15:47:55 +01:00
Sébastien Theys 65344dbf1f [FIX] store: fix useStore update when mixing state and props changes
This commit also fixes an issue where updates would not be triggered when the
new and old revNumber where the same but concerned a different object.
2019-11-27 15:09:47 +01:00
Sébastien Theys a6f9b26057 [FIX] store: ensure useStore proper clean up on destroy 2019-11-27 15:09:47 +01:00
Géry Debongnie 6c753bb49f [FIX] qweb: scope t-key directive to node and subnodes
not to siblings

closes #503
2019-11-27 13:31:40 +01:00
Géry Debongnie 1f079b883e [IMP] store: prevent changes to store state in useStore return value
closes #500
2019-11-27 08:18:44 +01:00
Géry Debongnie 8fb35ed969 [FIX] owl: add prettier to devDependencies
also, run prettier on codebase

closes #498
2019-11-27 08:06:17 +01:00
Géry Debongnie a3f2d07b40 [DOC] add link to useSubEnv in environment doc 2019-11-26 22:32:25 +01:00
Sébastien Theys c993278c80 [IMP] store: allow useStore to return primitive types
Related to #404
2019-11-26 22:17:05 +01:00
Géry Debongnie 3aa586db43 [IMP] component: allow custom validator functions for props
closes #375
2019-11-22 15:43:22 +01:00
Géry Debongnie 14db513f3b [FIX] component: allow fragments in method mount
It is now possible to mount components in fragments

closes #494
2019-11-22 15:00:37 +01:00
Géry Debongnie 7bb04185c7 [FIX] playground: fix wms sample
It was crashing when we close a window, because there are two event
handlers for the click event on Window.  But when we close a window, we
actually do not want to update the z index.
2019-11-22 13:41:44 +01:00
Géry Debongnie 48744cfa87 [DOC] add hooks explanation, and testing/debug page
closes #372
closes #419
2019-11-22 13:41:44 +01:00
Géry Debongnie ff76747a05 [REL] v1.0.0-alpha4 2019-11-22 13:03:02 +01:00
Aaron Bohy 7e6b1a28a0 [IMP] qweb: throw error if t-component not used on a node t
Before this rev, using t-component on a div (for example) node
would silently ignore the div and replace it by the root node of
the component. A way to improve this was to create an extra div
node and to put the component inside it. However, it raised
questions: what do we do with other attributes set on this tag?
Do we apply them on the div, or on the component? In addition,
some of them only make sense on the component (e.g. props), so we
have to detect them. Whatever we would have decided, it wouldn't
have been obvious from the user point of view, so we chose not to
support it, and we thus now raise an error in this case.

Closes #487.
2019-11-21 17:06:35 +01:00
Aaron Bohy 58f6724194 [FIX] component: reset isRendered when reusing fiber
It is important to reset the isRendered flag to false to ensure
that other (subsequent) simultaneous calls to render will be
skipped, as the currentFiber is actually no yet (re-)rendered.

Closes #483
2019-11-21 15:35:30 +01:00
Aaron Bohy 3c0f8ac76f [IMP] component: introduce OwlEvent
Closes #485
2019-11-21 13:12:22 +01:00
Géry Debongnie cb11c0118c [FIX] context tests: make them deterministic
We learn something new every day, and today is no exception. Promise.all
of a pending promise does not actually resolve as fast as possible after
the initial promise is completed, but a small delay after, because it is
put on the macrotask queue or something like that (I assume)

closes #480
2019-11-20 15:44:12 +01:00
Géry Debongnie 27b4eece66 [FIX] component: display correct error in some case
It could happen that a component would crash. But then, the __render
code tried to copy the class properties into the vnode, which does not
exist, creating a new error.

So, the solution is to process the classObj only in the successful part
of the render call, which then will not crash, so the normal error
handling will occur.
2019-11-20 14:35:25 +01:00
Aaron Bohy 2e3e8cd603 [FIX] component: destroy not yet mounted components (2)
Following 2922cee6ea

Previous commit was not correct: used children with a cancelled
fiber (for instance, with a new currentFiber) could be destroyed.
This commit fixes the issue differently, by directly detecting
children that are not used anymore (and not mounted), and destroy
them directly (no need to wait for willStart promise to be resolved
anymore).
2019-11-20 14:31:03 +01:00
Aaron Bohy 28ee790b3e [FIX] context: always remove subscription when destroyed
Before this rev., components destroyed before being mounted didn't
stop listening to the context changes.

Closes #476
2019-11-20 11:43:14 +01:00
Aaron Bohy 2922cee6ea [FIX] component: can destroy not yet mounted components
Sub-issue of #476
2019-11-20 11:43:14 +01:00
Géry Debongnie 7749fd3b96 [FIX] component: remove stale fiber in scheduler
Owl takes care of not rendering a component that will be unmounted
immediately after, and it works.  However, it also should clean
up the newly added fiber.
2019-11-20 11:24:32 +01:00
Géry Debongnie f44b9a38ae [IMP] component: better coordination for errors in rendering
part of #410
2019-11-20 11:24:32 +01:00
Géry Debongnie c7773bfd2a [FIX] qweb/component: fix scoping issue with t-model and t-foreach
Without this fix, the handler set by t-model did not capture properly
the expression that needs to be updated.

closes #474
2019-11-19 13:15:34 +01:00
Géry Debongnie bd39797f17 [FIX] component: propagate errors to caller
Errors from mounted/willPatch/patched should be returned to caller
(either mount or render functions)

part of #410
2019-11-18 14:29:54 +01:00
Géry Debongnie 94a595ef5b [FIX] qweb: add support for typeof expression
closes #469
2019-11-18 07:57:43 +01:00
Lucas Perais (lpe) f83846e054 [FIX] package, vdom: condition always true, TS 3.7.2
Before this commit, a condition deemed always true made
the building crash. The happened with TypeScript == 3.7.2

After this commit, the required typescript version has been
changed, as well as the incriminated true condition
2019-11-15 13:10:02 +01:00
Géry Debongnie a97144366d [FIX] playground: fix downloaded app code
The generated downloaded code from the playground did not work because
it did not load templates into QWeb anymore.

closes #461
2019-11-15 11:19:27 +01:00
Aaron Bohy 79983de4ed [FIX] context: notify changes even if owner not mounted
Closes #444
2019-11-15 07:34:15 +01:00
Géry Debongnie 54e1734f2f [IMP] component: display better error message if render is empty
closes #446
2019-11-14 16:40:07 +01:00
Géry Debongnie 4aea093ed9 [FIX] tools: fix benchmark for v0.24.0
It was not using the correct owl version
2019-11-14 15:51:53 +01:00
Géry Debongnie 56087b95cd [REF] observer: notify subscribers directly
This rev. moves the logic of batching the notifications of changes
occurring in the same microtask tick, from the observer, to the
listeners (the context, and the render function of components).
By doing so in render, we ensure that similar renderings are
batched in a single one, wherever they come from (state change,
store change, direct call...).
2019-11-14 15:12:31 +01:00
Aaron Bohy e5940b4b6b [FIX] component: concurrent calls to mount and render
Closes #450
2019-11-14 15:12:31 +01:00
Géry Debongnie bbdc9d90d7 [FIX] qweb: allow mixing body and expression variables
closes #445
2019-11-14 14:54:27 +01:00
Géry Debongnie f2b3ebd1ec [FIX] component: properly set currentFiber to null in all cases
Whenever a rendering is completed, we need to reset the currentFiber
to null to make sure all subsequent renderings will not be
confused.

However, the way it was done before this commit was wrong in a
specific situation: we resetted the currentFiber to null in the
patch method.  The idea was that this method was called every time
the component completes a rendering. But this is not true: it
can happen that a component is unmounted and remounted without
changes.  Then the component will be patched, but if there is no
change, it will not call recursively the patched methods of its
children.

So, we need to choose a better place to reset it to null.

closes #454
2019-11-14 11:56:13 +01:00
Géry Debongnie 55bb09ba1a [FIX] component: fix invalid prop validation situations
closes #453
2019-11-14 09:27:09 +01:00
Géry Debongnie d249f50d09 [REL] v1.0.0-alpha3 2019-11-13 15:43:47 +01:00
Géry Debongnie 749f0063ea [FIX] component: properly validate optional types in objects
closes #440
2019-11-13 15:36:46 +01:00
Géry Debongnie 263f31fea9 [DOC] miscellaneous improvements
including:

- add a link to the tutorial in the main readme page
- remove learning page on 'env', move content on reference page
- add dynamic sub components section in component page
- add reference page on props
- split qweb page into engine/language pages
- move t-key information into qweb language page
2019-11-13 08:49:25 +01:00
Géry Debongnie b891ae7de4 [REM] tools: remove error handling in playground
There was some code to attempt to display the error in the right
pane, whenever it occured in the app early phase.

However, it made it much harder to properly handle all cases. It could
silently swallow some errors (if more than one was done), And it was
not behaving the same way in Firefox and in Chrome.
2019-11-13 08:49:25 +01:00
Géry Debongnie c5a80497ac [FIX] tooling: properly set playground in dev mode
It was not in dev mode since changes to config system
2019-11-13 08:49:25 +01:00
Aaron Bohy e47f604449 [FIX] component: concurrent rendering issue
Resolved rendering with cancelled fiber for (not yet) destroyed
component -> Cannot read property 'sel' of null

Closes #421
2019-11-12 10:01:12 +01:00
Aaron Bohy 7e721a96b5 [IMP] hooks: useRef on component: set el anyway
Closes #437
2019-11-06 17:06:37 +01:00
Aaron Bohy af4f372506 [REF] component: mount: remove renderBeforeRemount option
Re-render the component all the time before remounting it, which
seems safer anyway.

In the test, we had to add a nextTick to wait for the changes to
be notified (before, we waited thanks to the additional call to
mount). If we don't do the nextTick, mount is called, and render
is called right after (before the promise returned by mount is
resolved). This scenario doesn't work right now (see #441).

Closes #381
2019-11-06 16:26:12 +01:00
Aaron Bohy 5731358607 [IMP] component: expose scheduler instance
Closes #433
2019-11-05 15:11:13 +01:00
Géry Debongnie 1226f015d7 [DOC] add a tutorial (TodoApp) 2019-11-04 16:50:16 +01:00
Géry Debongnie b211e75140 [IMP] store: display error if no store found 2019-11-04 08:41:29 +01:00
Géry Debongnie 8d1dd06340 [IMP] component: error when mounted on invalid target 2019-11-04 08:41:29 +01:00
Géry Debongnie bd3c1265d3 [REL] v1.0.0-alpha2 2019-11-01 09:12:33 +01:00
Géry Debongnie 2413c98f50 [REF] component: small optimization in rare cases
When we have default props, we do not need to allocate a new
props object and copy it. This commit simply modify the props
object in place, which is fine, since the props object is always
a new one any way, generated by the directive.
2019-11-01 09:03:30 +01:00
Géry Debongnie d74b5a03db [IMP] component: add props on root components
This is a partial revert of commit 7d249d6f09.

The reason is that this changes made it much harder to unit test
components.  Before, we could simply instantiate a component
like this:
const my|Comp = new MyComponent(null, props);

and then simply test it.  This commit reestablish that possibility.
2019-11-01 09:03:27 +01:00
Géry Debongnie 83532db48f [DOC] adapt documentation to env changes
part of #430
2019-11-01 09:03:27 +01:00
Géry Debongnie 534152eff7 [REF] component: small improvement
and run prettier
2019-11-01 08:39:35 +01:00
Aaron Bohy 05a678c039 [IMP] component: get env from constructor
closes #430
2019-11-01 08:39:35 +01:00
Géry Debongnie 8fbf2172c5 [DOC] slightly reorganize main documentation page 2019-10-31 12:32:26 +01:00
Géry Debongnie ea1376d0ca [FIX] tools: make benchmarks work on firefox
Firefox does not support the static keyword yet, so the benchmark code should be modified to make it work.
2019-10-31 12:32:26 +01:00
Géry Debongnie ba483b6e2c [FIX] component tests: make them work on windows
For an unknown reason, it looks like the way ticks, micro task ticks or other subtle scheduling issues are different on a windows machine (it may be because of other difference, such as a specific version of nodejs).

Anyway, I could not find the cause of the issue, but
simply waiting an extra microtask tick seems to work.
2019-10-31 12:32:26 +01:00
Géry Debongnie 2fc71cfb62 [REL] v1.0.0-alpha
v1.0alpha

The Alpha release!

Owl is finally getting stable. This relase is all about cleaning Owl
API.  We
are pretty happy with the current state, and hopefully, we won't have to
make
any non trivial change for a while.

QWeb

- add an option to setup a translate function
- implement `t-key` with a directive (it now works on `t` tags)

Component

- properly handle errors in `mount` and `render`
- fix: make sure props are validated in all cases
- fix: make sure default props are applied at the proper time
- fix: better error handling with sub components
- imp: simplify constructor API: does not take an `env`
- imp: do not set `props` in root components
- remove `t-keepalive` directive

Context

- update components concurrently, instead of sequentially

Observer

- remove `revNumber` (and rename `deepRevNumber` into `revNumber`)

Router

- fix: preserve pathname in hash mode

Config

- create new config object, with `mode` and `env` keys

Playground

- log git commit hash in console
2019-10-30 16:35:45 +01:00
Aaron Bohy 08cb83149e [REM] component: remove t-keepalive directive
We don't see any usecase for it, it makes the code more complex,
and there were still potential unresolved concurrency issues with
it.

Part of task #295
2019-10-30 16:35:45 +01:00
Aaron Bohy 7d249d6f09 [IMP] component: do not set props in root components
Also remove props from Fiber, as it was not useful anymore.
2019-10-30 15:09:10 +01:00
Géry Debongnie 0addca63a0 [DOC] update the documentation to new config and env
part of #306
2019-10-30 15:09:10 +01:00
Géry Debongnie 5ba73cc09d [DOC] move quick_start into learning/ sub folder 2019-10-30 15:09:10 +01:00
Aaron Bohy 9106c19066 [IMP] component: simplify constructor API
It now takes two arguments: parent (optional, only for non-root
components) and props (optional). In the case of the root component,
the env is taken from the config (config.defaultEnv). If it doesn't
exists, the default env is created on the fly.

Closes #306
2019-10-30 15:09:10 +01:00
Aaron Bohy 9f93da4765 [REF] config: move mode from owl.__info__ to owl.config
and move it to its own file.
2019-10-30 15:09:10 +01:00
Aaron Bohy 9e37b968e8 [FIX] component: error handling: rendering with sub components
Closes #425
2019-10-30 13:27:48 +01:00
Aaron Bohy fa6801b523 [REF] observer: remove revNumber
Closes #257
2019-10-30 11:13:52 +01:00
Géry Debongnie 9edf29a3a1 [IMP] context: group components by depth
closes #399
2019-10-29 16:02:51 +01:00
Géry Debongnie 6a434310ee [FIX] *: run prettier 2019-10-29 16:02:51 +01:00
Géry Debongnie e7967d0779 [FIX] package.json: improve prettier task
to take into account files at more than one level
2019-10-29 16:02:51 +01:00
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
312 changed files with 55681 additions and 83841 deletions
+27
View File
@@ -0,0 +1,27 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ master, owl-next ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run check-formatting
+9 -1
View File
@@ -15,6 +15,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
package-lock.json package-lock.json
yarn.lock
#ide's #ide's
.vscode .vscode
@@ -23,4 +24,11 @@ package-lock.json
node_modules node_modules
# Extras temp file # Extras temp file
/tools/owl.js /tools/owl.js
release-notes.md
.rpt2_cache
# useful in some cases
/temp
+612
View File
@@ -0,0 +1,612 @@
# Changelog
This document contains an overview of all changes between Owl 1.x and
Owl 2.x, with some pointers on how to update the code.
Note that some of these changes can be magically implemented (for example, by
patching the `setup` method of `Component` to auto register all the lifecycle
methods as hooks). This will be done for the transition period, but will be
removed after.
## Changes
**Components**
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- new `useEffect` hook
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
- breaking: component.el may be a text node, and is no longer `null` ([details](#9-componentel-may-be-a-text-node-and-is-no-longer-null))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
**Portal**
- portals can now have arbitrary content (no longer restricted to one single child)
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
**Slots**
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
**Miscellaneous**
- improved performance
- much simpler code
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state
- new App class to encapsulate a root Owl component (with the config for that application)
- new `Memo` component
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
- breaking: `Store` is removed ([details](#22-store-is-removed))
- breaking: `Router` is removed ([details](#23-router-is-removed))
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
## Details/Rationale/Migration
All changes are listed in no particular order.
### 1. component lifecycle methods are removed
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
than component methods. It enforces a single entry point to check all the useful lifecycle
calls (instead of it being scattered in the component definition). It feels more "modern".
Migration: lifecycle methods should be defined in the `setup`:
```js
class MyComponent extends Component {
mounted() {
// do something
}
}
```
should become:
```js
class MyComponent extends Component {
setup() {
onMounted(() => {
// do something
});
}
}
```
### 2. components can no longer be mounted in a detached dom element
Nor document fragment.
Rationale: it is actually very difficult to do it: this implies that a component
can be mounted more than once, that we need to check every time different status,
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
in practice. Removing this means that we have a much simpler mental model of what
happens.
Migration: well, not really easy. The code needs to be refactored in a different way.
### 3. **`t-set` will no longer work to define a slot**
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
can do it.
Rationale: it was left for compatibility reason, but was deprecated anyway.
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
Example:
```xml
<SideBar><t t-set="content">content</t></SideBar>
```
should become:
```xml
<SideBar><t t-set-slot="content">content</t></SideBar>
```
### 4. `mount` method API is simpler
Before, the `mount` method was used like this:
```js
await mount(Root, { target: document.body });
```
It is now simpler and takes the root component and a target argument:
```js
await mount(Root, document.body);
```
Rationale: the `mount` method is only useful anyway for small toy examples,
because real applications will need to configure the templates, the translations,
and other stuff. All complex usecases need to go through the new `App` class,
that encapsulates the root of an owl application.
### 5. components can no longer be instantiated and mounted by hand
In Owl 1, it was possible to instantiate a component by hand:
```js
const root = new Root();
await root.mount(document.body);
```
Now, it is no longer possible. All component instantiations should be done by
the owl framework itself.
Rationale: the `mount` method does not make sense for all non root components.
Also, the fact that it was possible for a component to be sometimes root,
sometimes a child made for a weird constructor signature. This changes makes it
simpler.
Migration: all code doing that should use either the `mount` method (if the use
case is simple enough, or the `App` class):
```js
const app = new App(Root);
app.configure({ templates: ..., ...});
await app.mount(document.body);
```
### 6. components can no longer be unmounted/remounted
Rationale: this is a very difficult feature to implement (it adds a lot of possible
state transitions), compared to its benefit.
Migration: all code using it should find a way to export and reimport the state
### 7. template name is no longer inferred from the class name
Before, it was possible to define a component without specifying its template:
```js
class Blabla extends Component {
// no static template here!
}
```
with the `Blabla` template. It also worked with subclasses. But then, this means
that the code had to look up all the super classes names to find the correct
template.
Rationale: in practice, it is not really useful, since all templates are usually
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
worth it.
Migration: simply explicitely defines the template key everytime:
```js
class Blabla extends Component {}
Blabla.template = "Blabla";
```
### 8. components no longer have a `shouldUpdate` method
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
because the reactivity system in Vue is smart enough to only rerender the minimal
subset of components that is subscribed to a piece of state. Now, Owl 2 features
a much more powerful reactivity system.
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
ideas may help:
- try to organize the state/architecture to minimize the number of state updates
- take advantage of the finer reactivity system. For example, if we have a list
of items, with a component for each item, we can write this:
```js
class Item extends Component {
setup() {
this.item = useState(this.props.item); // and only use this, not props.item
}
}
```
Doing so will make it that each `Item` component will register itself as an
observer of its own item, and will be the only component being rerendered when
its item object is updated.
- use the `Memo` component to wrap some piece of template. `Memo` memoize its
content, and only update itself if its props are different (shallow comparison):
```xml
<Memo a="state.a" b="state.b">
<t t-esc="state.a"/>
<t t-esc="state.b"/>
<t t-esc="state.c"/>
</Memo>
```
### 9. component.el may be a text node, and is no longer `null`
This comes from the fact that Owl 2 supports fragments (arbitrary content). When
it is not defined, it was `null` in Owl 1 and is `undefined` in owl 2.
### 10. style/class on components are now regular props
Before, it was possible to do this in a template:
```xml
<Child style="..." class="..."/>
```
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
now considered normal props.
Rationale: with the move to fragments, the semantics of where the style/class
attribute should be set is unclear. Also, it is actually very hard to implement
properly, in particular with higher order components. And another issue is that
it (slightly) breaks the encapsulation of behaviour from the `Child` component
perspective.
Migration: each component that wishes to be customized should explicitely add
the `class` and `style` attributes in its template. Also, the parent component
should be aware that since we are talking about props, it should be a javascript expression:
In parent:
```xml
<Child class="'o_my_god'"/>
```
and in child:
```xml
<div t-att-class="props.class">
...
</div>
```
### 11. components can no longer be mounted with position=self
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
necessary to support position=self does not work. This position also is not
compatible with the fact that a component can have a root `<div>` then later,
change it to something else, or even a text node.
Migration: no real way to do the same. Owl application needs to be appended or
prepended in something, maybe a `div`.
### 12. `t-on` does not work on components any more
In owl 1, it was possible to bind an event listener on a component tag in a
template:
```xml
<SomeComponent t-on-some-event="doSomething"/>
```
This does not work any more.
Rationale: with the support of fragments, there is no longer a canonical html
element that we can refer. So, this makes it difficult to implement correctly
and efficiently. Also, we noticed in practice that the event system was an issue
in some cases, when components need to communicate before they are mounted. In
those cases, the better solution is to directly use a callback. Also, another
conceptual issue with this is that it kind of breaks the component encapsulation.
The child component kind of leak its own implementation to the outside world.
Migration: a quick fix that may work in some cases is to simply bind the event
handler on a parent htmlelement. A better way to do it, if possible, is to change
the component API to accept explicitely a callback as props.
```xml
<SomeComponent onSomeEvent="doSomething"/>
```
### 13. Portal does no longer transfer DOM events
In Owl 1, a Portal component would listen to events emitted on its portalled
child, and redispatch them on itself. It no longer works.
Rationale: Portal now supports an arbitrary content (so, more than one child,
and potentially no html element), so it is already unclear what it should listen
to. Also, redispatching events was an hack. And this changes allows the portal
to render itself as a text node, which is nice. This is also in line with the
fact that modern Owl moves toward using callback instead of `t-on` for communication.
Migration: use callback if possible to communicate. Otherwise, use a sub env.
### 14. Portal does render as an empty text node instead of `<portal/>`
That is pretty nice. No real migration needed.
### 15. Context is removed
Context was an abstraction in Owl that was used to define some reactive state
and to let some components subscribe to it, then only them would be rerendered
if the context was updated. This has been removed.
Rationale: first, the Context api and code was kind of awkward, which is a sign
that the abstraction is not well thought. But the good news is that it is actually
completely replaced by the new reactivity system, which is even more powerful,
since it can tracks changes key by key.
Migration: replace all uses of Context with the new reactivity system.
```js
// somewhere, maybe in a service, or in the global env
const context = observe({some: "state"})
// in a component that would previously get a reference to the context:
setup() {
this.context = useState(context);
// now the component is subscribed to the context and will react to any
// change for any key read by the component, and only those changes
}
```
### 16. `env` is now totally empty
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
components would get a reference to their template function. It no longer works
that way: the `env` object is now totally empty (from the perspective of Owl).
It is now a user space concept, useful for the application.
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
the way components works internally.
Migration: there is no proper way to get an equivalent. The closest is to get
a reference to the root App using `this.__owl__.app`. If you need to do this,
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
### 17. `t-component` no longer accepts strings
In owl 1, we could write this:
```xml
<t t-component="Coucou"/>
```
This meant that Owl would look for the component class like this: `components["Coucou"]`,
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
is assumed to be an expression evaluating to a component class:
```js
class Parent extends Component {
static template = xml`<t t-component="Child"/>`;
Child = Child;
}
```
Rationale: it simply seems more consistent with the way directive works. Also,
the implementation is slightly simpler.
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
do the trick.
### 18. most exports are exported at top level
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
Rationale: this makes it easier to work with, instead of importing stuff from
`owl`, then `owl.hooks` and `owl.tags` for example.
Migration: all import code simply need to be slightly adapted.
### 19. Properties are no longer set as attributes
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
set as property and as attribute, so, they would be visible in the DOM:
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
`<input type="checkbox"/>`.
Rationale: this is actually simple to do, is faster, and makes more sense to me.
### 20. `t-foreach` should always have a corresponding `t-key`
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
warning in some cases, when it could detect that there was definitely not a `t-key`.
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
the tag with a `t-foreach` has to have a corresponding `t-key`.
Rationale: this makes it easier to avoid bugs.
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
a situation where there is really not a need for a `t-key`, you can still add
it with the `_index` suffix:
```xml
<div t-foreach="items" t-as="item" t-key="item_index">
...
</div>
```
### 21. `EventBus` api changed: it is now an `EventTarget`
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
it simply extends `EventTarget` (the native Dom class), so its implementation
is basically only 5 lines long. This means that it has now the usual DOM interface:
```js
bus.addEventListener('event-name', callback);
```
Rationale: it makes it easier to have just one interface to remember, it makes
the code simpler
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
### 22. `Store` is removed
The Store system had been abandoned in owl 2.
Rationale: first, it was complicated to maintain. Second, it was not really
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
to write a store, and it should not take much work. Also, this can be done in
user space (so, not necessarily at the framework level). Another point is that
the store API was invented before the hooks, then was still a little awkward.
Migration:
- rewrite the code not to use a store
- probably use the reactivity system instead and build a store class and a few
hooks on top of it.
### 23. `Router` is removed
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
Its API needs to be reworked, and we are not confident that it is a good
experience to use it. Also, it can be done in userspace (it does not need specific
integration at the framework level)
Migration: reimport all missing piece from the code in Owl 1.
### 24. transition system is removed
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
We feel like there should be a way to reimplement in userspace the simple cases.
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
that takes the ref, and add a fadeout class at initial render, then in mounted,
wait for a micro tick and remove it.
Migration: try to reimplement it manually.
### 25. no more global components or templates
It was possible in Owl 1 to register globally a component or a template. This is
no longer the case in Owl 2.
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
of a higher complexity. Users had to know that there was a magic mechanism. Also,
it was not used much in practice, and the cost of having to import manually components
is low. Finally, this can be mostly done in user space (for example, by subclassing
`Component`).
Migration: import manually all required global components, or find a way to organize
the code to do it.
### 26. `AsyncRoot` utility component is removed
Rationale: it was difficult to understand, never used, and not really useful.
It seems better to control the asynchrony of an application by simply controlling
how/when the state is updated, and how each component is loading/updating itself.
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
to fetch data in a higher order component, and using a `t-if/t-else` to display
either a fallback when the data is not ready, or the actual component with data
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
us so we can study this usecase.
### 27. `useSubEnv` only applies to child components
In Owl 1, a call to `useSubEnv` would define a new environment for the children
AND the component. It now only defines an environment for the children.
Rationale: This was a subtle cause for bugs: some code had to be rrun
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
### 28. `env` is now frozen
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
arbitrarily.
Rationale: it seems like the `env` object purpose is to have a global channel of
communication between components. It is however scary if anyone can add something
to it. The usual use case is to add something to the environment for some child
components. This use case still works with `useSubEnv`.
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
that the environment given to the App can initially contain anything.
### 29. `t-ref` does not work on component
Before, `t-ref` could be used to get a reference to a child component. It no
longer works.
Rationale: the possibility of having a ref to a child component breaks the
encapsulation provided by Owl components: a child component now has a private
and a public interface. Another issue is that it may be unclear when the ref
should be set: is the component active on setup, or on mounted? Also, it is
kind of awkward to implement.
Migration: the `env` and `props` should provide a communication channel wide enough:
the sub component can expose its public API by calling a callback at the proper
timing, or by triggering an event.
### 30. `t-on` does not accept expressions, only functions
In Owl 1, it was possible to define simple expressions inline, in a template:
```xml
<button t-on-click="state.value = state.value + 1">blabla</button>
<button t-on-click="someFunction(someVar)">blabla</button>
```
This does not work anymore. Now, the `t-on` directive assumes that what it get is
a function.
Rationale: the fact that owl 1 had to support expressions meant that it was not
possible to properly inject the event in general. With this restriction, Owl 2
can support more general use cases. Also, the examples above can simply be
wrapped in a lambda function.
Migration: use lambda functions. For example, the two examples above can be
adapted like this:
```xml
<button t-on-click="() => state.value = state.value + 1">blabla</button>
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
```
### 31. components can now have arbitrary content
Before Owl 2, components had to limit themselves to one single htmlelement as
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
So, the following template works for components:
```xml
<div>1</div>
<div>2</div>
hello
```
### 32. `renderToString` on QWeb has been removed
Rationale: the `renderToString` function was a qweb method, which made sense because
the qweb instance knew all templates. But now, the closest analogy is the `App`
class, but it is not as convenient, since the `app` instance is no longer visible
to components (while before, `qweb` was in the environment).
Also, this can easily be done in userspace, by mounting a component in a div. For example:
```js
export async function renderToString(template, context) {
class C extends Component {
static template = template;
}
const div = document.createElement('div');
document.body.appendChild(div);
const component = await mount(C, div);
const result = div.innerHTML;
app.destroy();
div.remove();
return result;
}
+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.
+103 -75
View File
@@ -1,102 +1,130 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1> <h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
_A web framework for structured, dynamic and maintainable applications_ [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_Class based components with hooks, reactive state and concurrent mode_
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
## Project Overview ## Project Overview
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo [Odoo](https://www.odoo.com/) for its products. Owl is a modern
related projects. OWL's main features are: framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom) - a declarative component system,
- a store implementation (for state management) - a reactivity system based on hooks,
- a small frontend router - concurrent mode by default,
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework. Owl components are defined with ES6 classes and xml templates, uses an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
asynchronous.
## OWL's Design Principles Quick links:
OWL is designed to be used in highly dynamic applications where changing - [documentation](#documentation),
requirements are common and code needs to be maintained by large teams. - [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
- [playground](https://odoo.github.io/owl/playground)
- **XML based**: templates are based on the XML format, which allows interesting
applications. For example, they could be stored in a database and modified
dynamically with `xpaths`.
- **templates compilation in the browser**: this may not be a good fit for all
applications, but if you need to generate dynamically user interfaces in the
browser, this is very powerful. For example, a generic form view component
could generate a specific form user interface for each various models, from a XML view.
- **no toolchain required**: this is extremely useful for some applications, if,
for various reasons (security/deployment/dynamic modules/specific assets tools),
it is not ok to use standard web tools based on `npm`.
Owl is not designed to be fast nor small (even though it is quite good on those
two topics). It is a no nonsense framework to build applications. There is only
one way to define components (with classes).
If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md).
## Example ## Example
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```xml
<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
```javascript ```javascript
class Counter extends owl.Component { const { Component, useState, mount, xml } = owl;
state = { value: 0 };
increment() { class Counter extends Component {
this.state.value++; static template = xml`
} <button t-on-click="() => state.value++">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
} }
const qweb = new owl.QWeb(TEMPLATES); class App extends Component {
const counter = new Counter({ qweb }); static template = xml`
counter.mount(document.body); <div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter };
}
mount(App, document.body);
``` ```
Note that we assume here that the xml templates are available in the `TEMPLATES` Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
string. More interesting examples can be found on the 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. [playground](https://odoo.github.io/owl/playground) application.
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
- [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
## Documentation ## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are: ### Learning Owl
- [Quick Start](doc/quick_start.md) Are you new to Owl? This is the place to start!
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
Found an issue in the documentation? A broken link? Some outdated information? - [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
Submit a PR! - [How to start an Owl project](doc/learning/quick_start.md)
- [How to test Components](doc/learning/how_to_test.md)
- [How to write Single File Components](doc/learning/how_to_write_sfc.md)
## License ### Reference
You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](doc/reference/animations.md)
- [Browser](doc/reference/browser.md)
- [Component](doc/reference/component.md)
- [Content](doc/reference/content.md)
- [Concurrency Model](doc/reference/concurrency_model.md)
- [Configuration](doc/reference/config.md)
- [Context](doc/reference/context.md)
- [Environment](doc/reference/environment.md)
- [Event Bus](doc/reference/event_bus.md)
- [Event Handling](doc/reference/event_handling.md)
- [Error Handling](doc/reference/error_handling.md)
- [Hooks](doc/reference/hooks.md)
- [Mounting a component](doc/reference/mounting.md)
- [Miscellaneous Components](doc/reference/misc.md)
- [Observer](doc/reference/observer.md)
- [Props](doc/reference/props.md)
- [Props Validation](doc/reference/props_validation.md)
- [QWeb Templating Language](doc/reference/qweb_templating_language.md)
- [QWeb Engine](doc/reference/qweb_engine.md)
- [Slots](doc/reference/slots.md)
- [Tags](doc/reference/tags.md)
- [Utils](doc/reference/utils.md)
### Other Topics
This section provides miscellaneous document that explains some topics
which cannot be considered either a tutorial, or reference documentation.
- [Owl architecture: the Virtual DOM](doc/miscellaneous/vdom.md)
- [Owl architecture: the rendering pipeline](doc/miscellaneous/rendering.md)
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
- [Why did Odoo built Owl?](doc/miscellaneous/why_owl.md)
## Installing Owl
Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
OWL is [GPL licensed](./LICENSE).
-1096
View File
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
# 🦉 How to test Components 🦉
## Content
- [Overview](#overview)
- [Unit Tests](#unit-tests)
## Overview
It is a good practice to test applications and components to ensure that they
behave as expected. There are many ways to test a user interface: manual
testing, integration testing, unit testing, ...
In this section, we will discuss how to write unit tests for components.
## Unit Tests
Writing unit tests for Owl components really depends on the testing framework
used in a project. But usually, it involves the following steps:
- create a test file: for example `SomeComponent.test.js`,
- in that file, import the code for `SomeComponent`,
- add a test case:
- create a real DOM element to use as test fixture,
- create a test environment
- create an instance of `SomeComponent`, mount it to the fixture
- interact with the component and assert some properties.
To help with this, it is useful to have a `helper.js` file that contains some
common utility functions:
```js
let lastFixture = null;
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture;
}
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
```
With such a file, a typical test suite for Jest will look like this:
```js
// in SomeComponent.test.js
import { SomeComponent } from "../../src/ui/SomeComponent";
import { nextTick, makeTestFixture } from '../helpers';
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = await mount(SomeComponent, fixture, { props });
// do some assertions
expect(...).toBe(...);
fixture.querySelector('button').click();
await nextTick();
// some other assertions
expect(...).toBe(...);
});
});
```
Note that Owl does wait for the next animation frame to actually update the DOM.
This is why it is necessary to wait with the `nextTick` (or other methods) to
make sure that the DOM is up-to-date.
+52
View File
@@ -0,0 +1,52 @@
# 🦉 How to write Single File Components 🦉
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 has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
helper.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
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.
+399
View File
@@ -0,0 +1,399 @@
# 🦉 How to start an Owl project 🦉
## Content
- [Overview](#overview)
- [Simple html file](#simple-html-file)
- [With a static server](#with-a-static-server)
- [Standard Javascript project](#standard-javascript-project)
## Overview
Each software project has its specific needs. Many of these needs can be solved
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
Because of that, it is usually not simple to just start a project. Some
frameworks provide their own tooling to help with that. But then, you have to
integrate and learn how these applications work.
Owl is designed to be used with no tooling at all. Because of that, Owl can
"easily" be integrated in a modern build toolchain. In this section, we will
discuss a few different setups to start a project. Each of these setups has
advantages and disadvantages in different situations.
## Simple html file
The simplest possible setup is the following: a simple javascript file with your
code. To do that, let us create the following file structure:
```
hello_owl/
index.html
owl.js
app.js
```
The file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
is a single javascript file which export all Owl into the global `owl` object.
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
```
And `app.js` should look like this:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
mount(Root, document.body);
```
Now, simply loading this html file in a browser should display a welcome message.
This setup is not fancy, but it is extremely simple. There are no tooling at
all required. It can be slightly optimized by using the minified build of Owl.
## With a static server
The previous setup has a big disadvantage: the application code is located in a
single file. Obviously, we could split it in several files and add multiple
`<script>` tags in the html page, but then we need to make sure the script are
inserted in the proper order, we need to export each file content in global
variables and we lose autocompletion across files.
There is a low tech solution to this issue: using native javascript modules.
This however has a requirement: for security reasons, browsers will not accept
modules on content served through the `file` protocol. This means that we need
to use a static server.
Let us start a new project with the following file structure:
```
hello_owl/
src/
index.html
main.js
owl.js
root.js
```
As previously, the file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
```
Not that the `main.js` script tag has the `type="module"` attribute. This means
that the browser will parse the script as a module, and load all its dependencies.
Here is the content of `root.js` and `main.js`:
```js
// root.js ----------------------------------------------------------------------
const { Component, mount, xml } = owl;
export class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js ---------------------------------------------------------------------
import { Root } from "./root.js";
mount(Root, document.body);
```
The `main.js` file imports the `root.js` file. Note that the import statement has
a `.js` suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion.
Now, to execute this code, we need to serve the `src` folder statically. A low
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
```
$ cd src
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
```
Another more "javascripty" way to do it is to create a `npm` application. To do
that, we can add the following `package.json` file at the root of the project:
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
```
We can now install the `serve` tool with the command `npm install`, and then,
start a static server with the simple `npm run serve` command.
## Standard Javascript project
The previous setup works, and is certainly good for some usecases, including
quick prototyping. However, it lacks some useful features, such as livereload,
a test suite, or bundling the code in a single file.
Each of these features, and many others, can be done in many different ways.
Since it is really not trivial to configure such a project, we provide here an
example that can be used as a starting point.
Our standard Owl project has the following file structure:
```
hello_owl/
public/
index.html
src/
components/
Root.js
main.js
tests/
components/
Root.test.js
helpers.js
.gitignore
package.json
webpack.config.js
```
This project as a `public` folder, meant to contain all static assets, such as
images and styles. The `src` folder has the javascript source code, and finally,
`tests` contains the test suite.
Here is the content of `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
```
Note that there are no `<script>` tag here. They will be injected by webpack.
Now, let's have a look at the javascript files:
```js
// src/components/Root.js -------------------------------------------------------
import { Component, xml, useState } from "@odoo/owl";
export class Root extends Component {
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
// src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl";
import { Root } from "./components/Root";
mount(Root, document.body);
// tests/components/Root.test.js ------------------------------------------------
import { Root } from "../../src/components/Root";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("Root", () => {
test("Works as expected...", async () => {
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
// tests/helpers.js ------------------------------------------------------------
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
```
Finally, here is the configuration files `.gitignore`, `package.json` and
`webpack.config.js`:
```
node_modules/
package-lock.json
dist/
```
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
```
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
};
```
With this setup, we can now use the following script commands:
```
npm run build # build the full application in prod mode in dist/
npm run dev # start a dev server with livereload
npm run test # run the jest test suite
```
+992
View File
@@ -0,0 +1,992 @@
# 🦉 OWL Tutorial: TodoApp 🦉
For this tutorial, we will build a very simple Todo list application. The app
should satisfy the following requirements:
- let the user create and remove tasks
- tasks can be marked as completed
- tasks can be filtered to display active/completed tasks
This project will be an opportunity to discover and learn some important Owl
concepts, such as components, store, and how to organize an application.
## Content
1. [Setting up the project](#1-setting-up-the-project)
2. [Adding a first component](#2-adding-a-first-component)
3. [Displaying a list of tasks](#3-displaying-a-list-of-tasks)
4. [Layout: some basic css](#4-layout-some-basic-css)
5. [Extracting Task as a subcomponent](#5-extracting-task-as-a-subcomponent)
6. [Adding tasks (part 1)](#6-adding-tasks-part-1)
7. [Adding tasks (part 2)](#7-adding-tasks-part-2)
8. [Toggling tasks](#8-toggling-tasks)
9. [Deleting tasks](#9-deleting-tasks)
10. [Using a store](#10-using-a-store)
11. [Saving tasks in local storage](#11-saving-tasks-in-local-storage)
12. [Filtering tasks](#12-filtering-tasks)
13. [The Final Touch](#13-the-final-touch)
14. [Final Code](#final-code)
## 1. Setting up the project
For this tutorial, we will do a very simple project, with static files and
no additional tooling. The first step is to create the following file structure:
```
todoapp/
index.html
app.css
app.js
owl.js
```
The entry point for this application is the file `index.html`, which should have
the following content:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</body>
</html>
```
Then, `app.css` can be left empty for now. It will be useful later on to style
our application. `app.js` is where we will write all our code. For now, let's
just put the following code:
```js
(function () {
console.log("hello owl", owl.__info__.version);
})();
```
Note that we put everything inside an immediately executed function to avoid leaking
anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
built to be bundled by other tools).
Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a
message such as `hello owl 2.x.y` in the console.
## 2. Adding a first component
An Owl application is made out of [components](../reference/component.md), with
a single root component. Let us start by defining a `Root` component. Replace the
content of the function in `app.js` by the following code:
```js
const { Component, mount, xml } = owl;
// Owl Components
class Root extends Component {
static template = xml`<div>todo app</div>`;
}
mount(Root, document.body);
```
Now, reloading the page in a browser should display a message.
The code is pretty simple: we define a component with an inline template, then
mount it in the document body.
Note 1: in a larger project, we would split the code in multiple files, with
components in a sub folder, and a main file that would initialize the application.
However, this is a very small project, and we want to keep it as simple as possible.
Note 2: this tutorial uses the static class field syntax. This is not yet
supported by all browsers. Most real projects will transpile their code, so this
is not a problem, but for this tutorial, if you need the code to work on every
browser, you will need to translate each `static` keyword to an assignation to
the class:
```js
class App extends Component {}
App.template = xml`<div>todo app</div>`;
```
Note 3: writing inline templates with the [`xml` helper](../reference/tags.md#xml-tag)
is nice, but there is no syntax highlighting, and this makes it very easy to
have malformed xml. Some editors support syntax highlighting for this situation.
For example, VS Code has an addon `Comment tagged template`, which, if installed,
will properly display tagged templates:
```js
static template = xml /* xml */`<div>todo app</div>`;
```
Note 4: Large applications will probably want to be able to translate templates.
Using inline templates makes it slightly harder, since we need additional tooling
to extract the xml from the code, and to replace it with the translated values.
## 3. Displaying a list of tasks
Now that the basics are done, it is time to start thinking about tasks. To
accomplish what we need, we will keep track of the tasks as an array of objects
with the following keys:
- `id`: a number. It is extremely useful to have a way to uniquely identify
tasks. Since the title is something created/edited by the user, it offers
no guarantee that it is unique. So, we will generate a unique `id` number for
each task.
- `text`: a string, to explain what the task is about.
- `isCompleted`: a boolean, to keep track of the status of the task
Now that we decided on the internal format of the state, let us add some demo
data and a template to the `App` component:
```js
class Root extends Component {
static template = xml/* xml */ `
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.text"/></span>
</div>
</t>
</div>`;
tasks = [
{
id: 1,
text: "buy milk",
isCompleted: true,
},
{
id: 2,
text: "clean house",
isCompleted: false,
},
];
}
```
The template contains a [`t-foreach`](../reference/qweb_templating_language.md#loops) loop to iterate
through the tasks. It can find the `tasks` list from the component, since the
component is the rendering context. Note that we use the `id` of each task as a
`t-key`, which is very common. There are two css classes: `task-list` and `task`,
that we will use in the next section.
Finally, notice the use of the `t-att-checked` attribute:
prefixing an attribute by [`t-att`](../reference/qweb_templating_language.md#dynamic-attributes) makes
it dynamic. Owl will evaluate the expression and set it as the value of the
attribute.
## 4. Layout: some basic css
So far, our task list looks quite bad. Let us add the following to `app.css`:
```css
.task-list {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.task {
font-size: 18px;
color: #111111;
}
```
This is better. Now, let us add an extra feature: completed tasks should be
styled a little differently, to make it clearer that they are not as important.
To do that, we will add a dynamic css class on each task:
```xml
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
```
```css
.task.done {
opacity: 0.7;
}
```
Notice that we have here another use of a dynamic attribute.
## 5. Extracting Task as a subcomponent
It is now clear that there should be a `Task` component to encapsulate the look
and behavior of a task.
This `Task` component will display a task, but it cannot _own_ the state of the
task: a piece of data should only have one owner. Doing otherwise is asking for
trouble. So, the `Task` component will get its data as a `prop`. This means that
the data is still owned by the `App` component, but can be used by the `Task`
component (without modifying it).
Since we are moving code around, it is a good opportunity to refactor the code
a little bit:
```js
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span>
</div>`;
static props = ["task"];
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml /* xml */`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>`;
static components = { Task };
tasks = [
...
];
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
mount(Root, document.body, {dev: true});
```
A lot of stuff happened here:
- first, we have now a sub component `Task`, defined on top of the file,
- whenever we define a sub component, it needs to be added to the static
[`components`](../reference/component.md#static-properties)
key of its parent, so Owl can get a reference to it,
- the `Task` component has a `props` key: this is only useful for validation
purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an
[error](../reference/props_validation.md). This is extremely
useful when refactoring components
- finally, to activate the props validation, we need to set Owl's
[mode](../reference/config.md#mode) to `dev`. This is done in the last argument
of the `mount` function. Note that this should be removed when an app is used in a real
production environment, since `dev` mode is slightly slower, due to extra
checks and validations.
## 6. Adding tasks (part 1)
We still use a list of hardcoded tasks. It's really time to give the user a way
to add tasks himself. The first step is to add an input to the `Root` component.
But this input will be outside of the task list, so we need to adapt `Root`
template, js, and css:
```xml
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
```
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', text);
// todo
}
}
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
```
We now have a working input, which log to the console whenever the user adds a
task. Notice that when you load the page, the input is not focused. But adding
tasks is a core feature of a task list, so let us make it as fast as possible by
focusing the input.
We need to execute code when the `Root` component is ready (mounted). Let's do
that using the `onMounted` hook. We will also need to get a reference to the
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
```xml
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
```
```js
// on top of file:
const { Component, mount, xml, useRef, onMounted } = owl;
```
```js
// in App
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
}
```
This is a very common situation: whenever we need to perform some actions depending
on the lifecycle of a component, we need to do it in the `setup` method, by using
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
then in the `onMounted` hook, we simply focus the html element.
## 7. Adding tasks (part 2)
In the previous section, we did everything except implement the code that actually
create tasks! So, let us do that now.
We need a way to generate unique `id` numbers. To do that, we will simply add a
`nextId` number in `App`. At the same time, let us remove the demo tasks in `App`:
```js
nextId = 1;
tasks = [];
```
Now, the `addTask` method can be implemented:
```js
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
if (text) {
const newTask = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
```
This almost works, but if you test it, you will notice that no new task is ever
displayed when the user press `Enter`. But if you add a `debugger` or a
`console.log` statement, you will see that the code is actually running as
expected. The problem is that Owl has no way of knowing that it needs to rerender
the user interface. We can fix the issue by making `tasks` reactive, with the
[`useState`](../reference/hooks.md#usestate) hook:
```js
// on top of the file
const { Component, mount, xml, useRef, onMounted, useState } = owl;
// replace the task definition in App with the following:
tasks = useState([]);
```
It now works as expected!
## 8. Toggling tasks
If you tried to mark a task as completed, you may have noticed that the text
did not change in opacity. This is because there is no code to modify the
`isCompleted` flag.
Now, this is an interesting situation: the task is displayed by the `Task`
component, but it is not the owner of its state, so ideally, it should not modify it.
However, for now, that's what we will do (this will be improved in a later step).
In `Task`, change the `input` to:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
```
and add the `toggleTask` method:
```js
toggleTask() {
this.props.task.isCompleted = !this.props.task.isCompleted;
}
```
## 9. Deleting tasks
Let us now add the possibility do delete tasks. This is different from the previous
feature: deleting task has to be done on the task itself, but the actual operation
need to be done on the task list. So, we need to communicate the request to the
`Root` component. This is usually done by providing a callback in a prop.
First, let us update the `Task` template, css and js:
```xml
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span>
</div>
```
```css
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
```
```js
static props = ["task", "onDelete"];
deleteTask() {
this.props.onDelete(this.props.task);
}
```
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
component:
```xml
<Task task="task" onDelete.bind="deleteTask"/>
```
```js
deleteTask(task) {
const index = this.tasks.findIndex(t => t.id === task.id);
this.tasks.splice(index, 1);
}
```
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
suffix that makes sure the function callback is bound to the component.
## 10. Using a store
Looking at the code, it is apparent that all the code handling tasks is scattered
all around the application. Also, it mixes UI code and business logic
code. Owl does not provide any high level abstraction to manage business logic,
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
Let us use it in our application to implement a central store. This is a pretty
large refactoring (for our application), since it involves extracting all task
related code out of the components. Here is the new content of the `app.js` file:
```js
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
nextId = 1;
tasks = [];
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
return reactive(new TaskList());
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = {
store: createTaskStore(),
};
mount(Root, document.body, { dev: true, env });
```
## 11. Saving tasks in local storage
Now, our TodoApp works great, except if the user closes or refresh the browser!
It is really inconvenient to only keep the state of the application in memory.
To fix this, we will save the tasks in the local storage. With our current
codebase, it is a simple change: we need to save tasks to local storage and
listen to any change.
```js
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
// ...
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
```
The key point is that the `reactive` function takes a callback that will be called
every time an observed value is changed. Note that we need to call the `saveTasks`
method initially to make sure we observe all current values.
## 12. Filtering tasks
We are almost done, we can add/update/delete tasks. The only missing feature is
the possibility to display the task according to their completed status. We will
need to keep track of the state of the filter in `Root`, then filter the visible
tasks according to its value.
```js
class Root extends Component {
static template = xml /* xml */`
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>`;
setup() {
...
this.filter = useState({ value: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) {
case "active": return tasks.filter(t => !t.isCompleted);
case "completed": return tasks.filter(t => t.isCompleted);
case "all": return tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
}
```
```css
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
Notice here that we set dynamically the css class of the filter with the object
syntax.
## 13. The Final Touch
Our list is feature complete. We can still add a few extra details to improve
the user experience.
1. Add a visual feedback when the user mouse is over a task:
```css
.task:hover {
background-color: #def0ff;
}
```
2. Make the text of a task clickable, to toggle its checkbox:
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
```
3. Strike the text of completed task:
```css
.task.done label {
text-decoration: line-through;
}
```
## Final code
Our application is now complete. It works, the UI code is well separated from
the business logic code, it is testable, all under 150 lines of code (template
included!).
For reference, here is the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</html>
```
```js
(function () {
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox"
t-att-id="props.task.id"
t-att-checked="props.task.isCompleted"
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="store.tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length">
/ <t t-esc="store.tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
this.filter = useState({ value: "all" });
}
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) {
case "active":
return tasks.filter((t) => !t.isCompleted);
case "completed":
return tasks.filter((t) => t.isCompleted);
case "all":
return tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(Root, document.body, { dev: true, env });
})();
```
```css
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task:hover {
background-color: #def0ff;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
.task.done {
opacity: 0.7;
}
.task.done label {
text-decoration: line-through;
}
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
```
@@ -3,8 +3,8 @@
OWL, React and Vue have the same main feature: they allow developers to build OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences. declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, 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 effort was put to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text. discussed, feel free to open an issue/submit a PR to correct this text.
## Content ## Content
@@ -14,8 +14,8 @@ discussed, feel free to open an issue/submit a PR to correct this text.
- [Tooling/Build Step](#toolingbuild-step) - [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating) - [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering) - [Asynchronous rendering](#asynchronous-rendering)
- [Reactiveness](#reactiveness) - [Reactivity](#reactivity)
- [State Management](#state-management) - [Hooks](#hooks)
## Size ## Size
@@ -24,22 +24,38 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
| Framework | Size (minified, gzipped) | | Framework | Size (minified, gzipped) |
| ------------------------ | ------------------------ | | ------------------------ | ------------------------ |
| OWL | 16kb | | OWL | 18kb |
| Vue + VueX | 30kb | | Vue + VueX | 30kb |
| Vue + VueX + Vue Router | 39kb |
| React + ReactDOM + Redux | 40kb | | React + ReactDOM + Redux | 40kb |
| jQuery | 30kb | | jQuery | 30kb |
Note that those comparisons are not entirely fair, because we do not compare
the same exact set of features. For example, VueX and Vue Router support more
advanced use cases.
## Class Based ## Class Based
Both React and Vue moved away from defining components with classes. They prefer Both React and Vue moved away from defining components with classes. They prefer
a more functional approach, in particular, with the new `hooks` mechanisms. a more functional approach, in particular, with the new `hooks` mechanisms.
This has some advantages and disadvantages. But the end result is that React 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 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 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 components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us). possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
It certainly looks like the world of UI frameworks is moving toward composition,
for many very good reasons. Owl is still good at composition (for example,
Owl supports slots, which is the primary mechanism to make generic reusable
components). But it can also use inheritance (and this is very important since
templates can also be inherited with `xpaths` transformations).
## Tooling/Build step ## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons, OWL is designed to be easy to use in a standalone way. For various reasons,
@@ -50,13 +66,23 @@ be used by simply adding a script tag to a page.
<script src="owl.min.js" /> <script src="owl.min.js" />
``` ```
In comparison, React encourages using JSX, In comparison, React encourages using JSX, which necessitate a build step, and
which necessitate a build step, and most Vue applications uses single file most Vue applications uses single file components, which also necessitate a build step.
components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it 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. also brings a lot of benefits. And React/Vue have both a large ecosystem.
Note that since Owl is not dependant on any external tool nor libraries, it is
very easy to integrate into any build toolchain. Also, since we cannot rely on
additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](../reference/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 ## Templating
OWL uses its own QWeb engine, which compiles templates on the OWL uses its own QWeb engine, which compiles templates on the
@@ -79,7 +105,8 @@ into javascript functions. Note that Vue has a separate build which includes the
template compiler. template compiler.
In contrast, most React applications do not use a templating language, but write 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 ```jsx
class Clock extends React.Component { class Clock extends React.Component {
@@ -98,6 +125,20 @@ 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: structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github! there is a syntax highlighter for jsx here on github!
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 ## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL This is actually a big difference between OWL and React/Vue: components in OWL
@@ -127,12 +168,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. 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 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 ## Reactivity
React has a simple model: whenever the state changes, it is React has a simple model: whenever the state changes, it is
replaced with a new state (via the setState method). Then, the DOM is patched. replaced with a new state (via the `setState` method). Then, the DOM is patched.
This is simple, efficient, and a little bit awkward to write. 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 Vue is a little bit different: it replace magically the properties in the state
@@ -145,92 +188,68 @@ with a `Proxy`, which means that it is totally transparent to the developers.
Adding new keys is supported. Once any part of the state has been changed, a Adding new keys is supported. Once any part of the state has been changed, a
rendering is scheduled in the next microtask tick (promise queue). rendering is scheduled in the next microtask tick (promise queue).
## State Management ## Hooks
Managing the state of an application is a tricky issue. Many solutions have [Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
been proposed these last few years. It also depends on the kind of application we the React world. They solve a lot of seemingly unconnected problems: attach
are talking about. A small application may not need much more than a simple reusable behavior to a component, in a composable way, extract stateful logic
object to contain its state. from a component or reuse stateful logic between component, without changing your
component hierarchy.
However, there are some common solutions for React and Vue: redux and vuex. Here is an example of the React `useState` hook:
Both of them are a centralized store that own the state, and they dictate how
the state can be mutated.
**Redux** ```js
import React, { useState } from "react";
In Redux, the state is mutated by reducers. Reducers are functions function Example() {
that modify the state by returning a different object: // Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
```javascript return (
... <div>
switch (action.type) { <p>You clicked {count} times</p>
case ADD_TODO: { <button onClick={() => setCount(count + 1)}>Click me</button>
const { id, content } = action.payload; </div>
return { );
...state, }
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false
}
}
};
}
``` ```
This is a little bit awkward to write, but this allows the component system to Because of the way React designed the hooks API, they only work for functional
check if a part of the state was changed. This is exactly what is done by the components. But in that case, they really are powerful. Every major React library
`connect` function: it create a _connected_ component, which is subscribed to is in the process of redesigning their API with hooks (for example,
the state and triggers a rerender if some part of the state was modified. [Redux](https://react-redux.js.org/next/api/hooks)).
**VueX** Vue 2 does not have hooks, but the Vue project is working on its next version,
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
This work is based on the new ideas introduced by React hooks.
VueX is based on a different principle: the state is mutated through From the way React and Vue introduce their hooks, it may look like hooks are not
some special functions (the mutations), which modify the state in place: 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.
```javascript Here is what the `Counter` example above look like in Owl:
function ({state}, payload) {
const { id, content } = payload;
const message = {id, content, completed: false};
state.messages.push(message)
}
```
This is simpler, but there is a little bit more happening behind the scene: ```js
each key from the state is silently replaced by getters and setters, and VueX import { Component, Owl } from "owl";
keeps track of who get data, and retrigger a render when it was changed. import { xml } from "owl/tags";
**Owl** class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
Owl store is a little bit like a mix of redux and vuex: it has actions (but not count = useState({ value: 0 });
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, by inheriting the `ConnectedComponent` class.
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() { increment() {
this.env.store.dispatch("increment"); this.state.value++;
} }
} }
```
const counter = new Counter({ store, qweb }); Since the Owl framework had hooks from early in its life, its main APIs
``` are designed to be interacted with hooks from the start. For example, the
`Context` abstraction.
+32
View File
@@ -0,0 +1,32 @@
# 🦉 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).
+180
View File
@@ -0,0 +1,180 @@
# 🦉 Why Owl ? 🦉
The common wisdom is that one should not reinvent the wheel, because that would
waste effort and resources. It is certainly true in many cases. A javascript
framework is a considerable investment, so it is quite logical to ask the question:
why did Odoo decide to make OWL instead of using a standard/well known framework,
such as React or Vue?
As you might expect, the answer to that question is not simple. But most of the
reasons discussed in this page are a consequence from a single fact: Odoo is
extremely modular.
This means, for example, that the core parts of Odoo are not aware, before runtime,
of what files will be loaded/executed, or what will be the state of the UI. Because
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
really an application with a user interface. It is an application which generates
a dynamic user interface. And most frameworks are not up to the task.
Betting on Owl was not an easy choice to make, because there certainly are a lot
of conflicting needs that we want to carefully balance. Choosing anything other
than a well known framework is bound to be controversial. This page will explain
some of the reason why we still believe that building Owl is a worthwile
endeavour.
## Strategy
It is true that we want to keep control of our technology, in the sense that we
do not want to depend on Facebook or Google, or any other large (or small)
company. If they decide to change their license, or to go in a direction that
will not work for us, this may be a problem. This is even more true because
Odoo is not a conventional javascript application, and our needs are probably
quite different as most other applications.
## Class components
It is clear that the biggest frameworks are moving away from class components.
There is an implicit assumption that class components are terrible, and that
functional programming is the way to go. React even goes as far as to say that
classes are confusing for developers.
While there is some truth to that, and to the fact that composition is certainly
a good mechanism for code reuse, we believe that classes and inheritance are
important tools.
Sharing code between generic components with inheritance is the way Odoo built
its web client. And it is clear that inheritance is not the root of all evils.
It is often a perfectly simple and appropriate solution. What matter most is
the architectural decisions.
Also, Odoo has another specific use out of class components: each method of a
class provides an extension point for addons. This may not be a clean architecture
pattern, but it is a pragmatic decision that served Odoo well: classes are
sometimes monkey-patched to add behaviour from the outside. A little bit like
mixins, but from the outside.
Using React or Vue would make it significantly harder to monkey patch components,
because a lot of the state is hidden in their internals.
## Tooling
React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user installs
or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolved at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!)
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag.
## Template based
Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity.
Because of that, we still expect to write our templates in an XML document.
Weirdly enough, no major framework uses XML to store templates, even though it
is extremely convenient.
So, using React or Vue means that we need to make a template compiler. For React,
that would be a compiler that would take a QWeb template, and convert it to a
React render function. For Vue, it would convert it to a Vue template. Then
we need to bundle the vue template compiler as well.
Not only this would be complex (compiling a templating language into another is
not an easy task), but it would negatively impact the developer experience as
well. Writing Vue or React components in a QWeb template would certainly be
awkward, and very confusing.
## Developer Experience
This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developers as
easy as possible.
While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional components, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal
keys. Svelte transform the code. React require that state transformations are
deep, and not shallow.
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
reactivity system is explicit, not implicit. The scoping rules are obvious. In
case of doubt, we err on the side of not implementing a feature.
It is certainly different from React or Vue, but at the same time, kind of
familiar for experienced developers.
## JIT compilation
There is also a clear trend in the frontend world to compile code
as much as possible ahead of time. Most frameworks will compile templates ahead
of time. And now Svelte is trying to compile the JS code away, so it can remove
itself from the bundle.
This is certainly reasonable for many usecases. However, this is not what Odoo
needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret an xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it
immediately.
## Reactivity
There are other design choices that we feel are not optimal in other frameworks.
For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not read-only,
and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory.
## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo wants a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions).
React has now an experimental concurrent mode, but it was not ready when Owl
started. Vue has not really an equivalent API (suspense is not what we need).
Also, React concurrent mode is complex to use. Concurrency was one of the rare
strong point of the former Odoo js framework (widgets), and we feel that Owl has
now a very strong concurrent mode, which is simple and powerful at the same time.
## Conclusion
This lengthy discussion showed that there are many small and not so small reasons
that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something
that is different. For a framework that makes choices compatible with Odoo.
And that is why we built Owl 🦉.
-50
View File
@@ -1,50 +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 three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
-101
View File
@@ -1,101 +0,0 @@
# 🦉 Quick Start 🦉
## Static Server
Let us assume that we have a static server running somewhere. We could then
simply add an html page with a few extra files.
### HTML and CSS
In a file `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My OWL App</title>
<link href="app.css" rel="stylesheet" />
<script src="owl-X.Y.Z.js"></script>
</head>
<body>
<div id="main"></div>
<script src="app.js" type="module"></script>
</body>
</html>
```
In `app.css`:
```css
button {
color: darkred;
font-size: 30px;
width: 220px;
}
```
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
### XML
In `templates.xml`:
```xml
<templates>
<button t-name="clickcounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
### JS
To build an application (or a sub-part of an application), we need two things:
- an environment: it is the global context in which we are working. It needs to
contain a QWeb instance (preloaded with templates), and anything else that we
need. In practice, it could context some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode).
- a description of the user interface: there should be a root component, which can
have sub components
Here are a few steps that we may take to get started:
- get the templates
- create a qweb engine, with the templates
- create an environment
- create an instance of the root component
- mount the root component to a DOM element
Let us now add the javascript to make it work, in `app.js`:
```javascript
class ClickCounter extends owl.Component {
constructor() {
super(...arguments);
this.template = "clickcounter";
this.state = { value: 0 };
}
increment() {
this.state.value++;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const counter = new ClickCounter(env);
const target = document.getElementById("main");
await counter.mount(target);
}
start();
```
-49
View File
@@ -1,49 +0,0 @@
# 🦉 OWL Documentation 🦉
## Owl Content
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
```
owl
Component
QWeb
core
EventBus
Observer
router
Link
Router
store
Store
ConnectedComponent
utils
debounce
escape
loadJS
loadTemplates
whenReady
```
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [Event Bus](event_bus.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)
- [Store](store.md)
- [Utils](utils.md)
- [Virtual DOM](vdom.md)
## Learning Resources
- [Quick Start](quick_start.md)
## Miscellaneous
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
@@ -30,7 +30,7 @@ btn {
} }
``` ```
will produce a nice flash effect whenever the user click (or activate with the will produce a nice flash effect whenever the user clicks (or activates with the
keyboard) the button. keyboard) the button.
## CSS Transitions ## CSS Transitions
@@ -47,26 +47,25 @@ the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help. directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen: sequence of events will happen:
At node insertion: At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly - the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM, when the node is inserted into the DOM.
- on the next animation frame: the css class `name-enter` will be removed and the - on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css class `name-enter-to` will be added (so they can be used to trigger css
transition effects), transition effects).
- the css class `name-enter-active` will be removed whenever a css transition - at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
ends.
At node destruction: At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the - the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM, node is removed to the DOM.
- the css class `name-leave` will be removed on the next animation frame (so it - on the next animation frame: the css class `name-leave` will be removed and the
can be used to trigger css transition effects), class `name-leave-to` will be added (so they can be used to trigger css
- the css class `name-leave-active` will be removed whenever a css transition transition effects).
ends. Only then will the element be removed from the DOM. - at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
For example, a simple fade in/out effect can be done with this: For example, a simple fade in/out effect can be done with this:
@@ -91,6 +90,38 @@ The `t-transition` directive can be applied on a node element or on a component.
Notes: Notes:
- more information on animations are available [here](animations.md). Owl does not support more than one transition on a single node, so the
- Owl does not support more than one transition on a single node, so the `t-transition` expression must be a single value (i.e. no space allowed).
`t-transition` expression must be a single value (i.e. no space allowed)
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
+33
View File
@@ -0,0 +1,33 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+855
View File
@@ -0,0 +1,855 @@
# 🦉 OWL Component 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Reactive System](#reactive-system)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`setup()`](#setup)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component)
- [Composition](#composition)
- [Form Input Bindings](#form-input-bindings)
- [References](#references)
- [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components)
- [SVG Components](#svg-components)
## Overview
OWL components are the building blocks for user interface. They are designed to be:
1. **declarative:** the user interface should be described in terms of the state
of the application, not as a sequence of imperative steps.
2. **composable:** each component can seamlessly be created in a parent component by
a simple tag or directive in its template.
3. **asynchronous rendering:** the framework will transparently wait for each
sub components to be ready before applying the rendering. It uses native promises
under the hood.
4. **uses QWeb as a template system:** the templates are described in XML
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_templating_language.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
## Example
Let us have a look at a simple component:
```javascript
const { useState } = owl.hooks;
class ClickCounter extends owl.Component {
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
```xml
<button t-name="ClickCounter" 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 shows how a component should be defined: it simply subclasses the
Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here,
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
[observed](observer.md), and any change to it will cause a rerendering.
## Reference
An Owl component is a small class which represents a component or some UI element.
It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
will be used to render the component template.
Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestors).
### Reactive system
OWL components are normal javascript classes. So, changing a component internal
state does nothing more:
```js
class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = { value: 0 };
increment() {
this.state.value++;
}
}
```
Clicking on the `Counter` component defined above will call the `increment`
method, but it will not rerender the component. To fix that, one could add an
explicit call to `render` in `increment`:
```js
increment() {
this.state.value++;
this.render();
}
```
However, it may be simple in this case, but it quickly become cumbersome, as a
component get more complex, and its internal state is modified by more than one
method.
A better way is to use the reactive system: by using the `useState` hook (see the
[hooks](hooks.md) section for more details), one can make Owl react to state
changes. The `useState` hook generates a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change. So, the `Counter` example above can be improved like this:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Obviously, we can call the `useState` hook more than once:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<div>
<span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span>
<span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span>
</div>`;
counter1 = useState({ value: 0 });
counter2 = useState({ value: 0 });
increment(counter) {
counter.value++;
}
}
```
Note that hooks are subject to one important [rule](hooks.md#one-rule): they need
to be called in the constructor.
### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
component is not mounted.
- **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
- **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation,
the parent component gives a `user` and a `color` value to the `ChildComponent`.
```xml
<div>
<ChildComponent user="state.user" color="color">
</div>
```
Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component (otherwise you risk
unintended effects, since the parent may not be aware of the change)!!
The `props` can be modified dynamically by the parent. In that case, the
component will go through the following lifecycle methods: `willUpdateProps`,
`willPatch` and `patched`.
### Static Properties
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
make it easy to define an inline template.
* **`components`** (Object, optional): if given, this is an object that contains
the classes of any sub components needed by the template. This is the main way
used by Owl to be able to create sub components.
```js
class ParentComponent extends owl.Component {
static components = { SubComponent };
}
```
* **`props`** (Object, optional): if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](props_validation.md) for more information.
```js
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true,
};
}
```
- **`defaultProps`** (Object, optional): if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
}
```
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
which is used to inject stylesheet whenever the component is visible on the
screen.
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
component.
### Methods
We explain here all the public methods of the `Component` class.
- **`mount(target, options)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `options` argument is an optional object with a `position` key. The
`position` key can have three possible values: `first-child`, `last-child`, `self`.
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
Note that if a component is mounted, unmounted and remounted, it will be
automatically re-rendered to ensure that changes in its state (or something
in the environment) will be taken into account.
If a component is mounted inside an element or a fragment which is not in the
DOM, then it will be rendered fully, but not active: the `mounted` hooks will
not be called. This is sometimes useful if we want to load an application in
memory. In that case, we need to mount the root component again in an element
which is in the DOM:
```js
const app = new App();
await app.mount(document.createDocumentFragment());
// app is rendered in memory, but not active
await app.mount(document.body);
// app is now visible
```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
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 component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components. Since this is an
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
called directly (except maybe on the root component), but should be done by the
framework instead.
Obviously, these methods are reserved for Owl, and should not be used by Owl
users, unless they want to override them. Also, Owl reserves all method names
starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change.
### Lifecycle
A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of
a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- |
| **[setup](#setup)** | setup |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling page](error_handling.md)) |
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 = useState({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 {
state = useState({ value: 0 });
...
}
```
Hook functions can be called in the constructor.
#### `setup()`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is a valid method to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
#### `willStart()`
willStart 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. 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.
#### `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.
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
Doing so will cause a rerender, which will not be perceptible by the user, but
will slightly slow down the component.
#### `willUpdateProps(nextProps)`
The willUpdateProps is an asynchronous hook, called just before new props
are set. This is useful if the component needs to perform an asynchronous task,
depending on the props (for example, assuming that the props are
some record Id, fetching the record data).
```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
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM.
#### `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 component is
not in the DOM.
Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in
turn will cause other calls to the `patched` method. So, we need to be particularly
careful at avoiding endless cycles.
#### `willUnmount()`
willUnmount is a hook that is called each time just before a component is unmounted from
the DOM. This is a good place to remove listeners, for example.
```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`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the page on
[error handling](error_handling.md).
### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
directive) in a template. There is however an obvious exception: the root component
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const app = new App();
app.mount(document.body);
```
The root component does not have a parent nor `props` (see note below). It will be setup with an
[environment](environment.md) (either the `env` defined on its class, or a
default empty environment).
Note: a root component can however be given a `props` object in its constructor,
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
object, managed by Owl (so, for example, it will never be updated).
### Composition
The example above shows a QWeb template with a sub component. In a template,
components are declared with a tagname corresponding to the class name. It has
to be capitalized.
```xml
<div t-name="ParentComponent">
<span>some text</span>
<MyComponent info="13" />
</div>
```
```js
class ParentComponent extends owl.Component {
static components = { MyComponent: MyComponent};
...
}
```
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
after the span. The `info` key will be added to the subcomponent's `props`. Each
`props` is a string which represents a javascript (QWeb) expression, so it is
dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means
that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml
<div t-name="ParentComponent">
<ChildComponent count="state.val" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent };
state = useState({ val: 4 });
}
```
Whenever the template is rendered, it will automatically create the subcomponent
`ChildComponent` at the correct place. It needs to find the reference to the
actual component class in the special static `components` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically. See the [props section](props.md)
for more information.
**CSS and style:** Owl allows the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element.
```xml
<div t-name="ParentComponent">
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or other custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Form Input Bindings
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts the following modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### References
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml
<div>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
```js
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
The `useRef` hook can also be used to get a reference to an instance of a sub
component rendered by Owl. In that case, we need to access it with the `comp`
property instead of `el`:
```xml
<div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
```
```js
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
static components = { SubComponent };
subRef = useRef("sub");
doSomething() {
this.subRef.comp.doSomeThingElse();
}
}
```
Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
Note that the `t-component` directive can only be used on `<t>` nodes.
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`;
class MyComponent extends Component {
static template = xml`
<div>
<t t-call=${Welcome}/>
<div>something</div>
</div>
`;
}
```
The way this works is that sub templates are inlined, and have access to the
ambient context. They can therefore access `props`, and any other part of the
caller component.
### SVG Components
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] },
],
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
+181
View File
@@ -0,0 +1,181 @@
# 🦉 Concurrency Model 🦉
## Content
- [Overview](#overview)
- [Rendering Components](#rendering-components)
- [Semantics](#semantics)
- [Asynchronous Rendering](#asynchronous-rendering)
## Overview
Owl was designed from the very beginning with asynchronous components. This comes
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
methods, it is possible to build complex highly concurrent applications.
Owl concurrent mode has several benefits: it makes it possible to delay the
rendering until some asynchronous operation is complete, it makes it possible
to lazy load libraries, while keeping the previous screen completely functional.
It is also good for performance reasons: Owl uses it to only apply the result of
many different renderings only once in an animation frame. Owl can cancel
a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concurrent rendering works in a general way.
## Rendering Components
The word _rendering_ is a little vague, so, let us explain more precisely the
process by which Owl components are displayed on a screen.
When a component is mounted or updated, a new rendering is started. It has
two phases: _virtual rendering_ and _patching_.
### Virtual rendering
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
virtual DOM.
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
will need to be called), or updated (which is done with the `willUpdateProps`
method). This is completely a recursive process: a component is the root of a
component tree, and each sub component needs to be (virtually) rendered.
### Patching
Once a rendering is complete, it will be applied on the next animation frame.
This is done synchronously: the whole component tree is patched to the real
DOM.
## Semantics
We give here an informal description of the way components are created/updated
in an application. Here, ordered lists describe actions that are executed
sequentially, bullet lists describe actions that are executed in parallel.
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
```
A
/ \
B C
/ \
D E
```
Here is what happen whenever we mount the root
component (with some code like `app.mount(document.body)`).
1. `willStart` is called on `A`
2. when it is done, template `A` is rendered.
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. each components are patched into a detached DOM element, in the following order:
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
in one pass)
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all components in the following
order: `E`, `D`, `C`, `B`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new component `F`.
So, the component tree should look like this:
```
A
/ \
B C
/ \
D F
```
Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `F` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. components `F`, `D` are patched in that order
5. component `C` is patched, which will cause recursively:
1. `willUnmount` hook on `E`
2. destruction of `E`,
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
There are two different common problems with Owl asynchronous rendering model:
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
+44
View File
@@ -0,0 +1,44 @@
# 🦉 Config 🦉
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It provides two settings:
- [`mode`](#mode) (default value: `prod`),
- [`enableTransitions`](#enabletransitions) (default value: `true`).
## 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, it is sometimes necessary
to have better information on what is going on, this is the purpose
of the `dev` mode.
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.config.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.
## `enableTransitions`
Transitions are usually nice, but they can cause issues in some specific cases,
such as automated tests. It is uncomfortable having to wait for a transition
to end before moving to the next step.
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
To do that, one only needs to set the `enableTransitions` flag to false:
```js
owl.config.enableTransitions = false;
```
Note that it suffers from the same drawback as the "dev" mode: all compiled
templates, if any, will keep their current behaviours.
+36
View File
@@ -0,0 +1,36 @@
# 🦉 Owl Content 🦉
Here is a complete visual representation of everything exported by the `owl`
global object.
For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`.
```
browser
Component misc
Context AsyncRoot
QWeb Portal
mount
useState tags
config css
mode xml
core utils
EventBus debounce
Observer escape
hooks loadJS
onWillStart loadFile
onMounted shallowEqual
onWillUpdateProps whenReady
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useComponent
useEnv
useSubEnv
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+105
View File
@@ -0,0 +1,105 @@
# 🦉 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 });
App.env.deviceContext = 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="">
a more advanced user interface
</t>
</div>`;
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.
+136
View File
@@ -0,0 +1,136 @@
# 🦉 Environment 🦉
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
- [Special keys](#special-keys)
## Overview
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
a root component is created, it is assigned an environment (see
[below](#setting-an-environment) for more info on this). This environment is
then automatically given to each sub component (and accessible in the `this.env`
property).
```
Root
/ \
A B
```
This way, all components share the same `QWeb` instance. Owl internally requires
that the environment has a `qweb` key which maps to a
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
render each templates in this specific component tree. Note that if no `QWeb`
instance is provided, Owl will simply generate it on the fly.
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.
## Setting an environment
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
this will return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
const env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
mount(App, { target: document.body, env });
```
It is also possible to simply share an environment between all root components,
by simply doing this:
```js
Component.env = myEnv; // will be the default env for all components
```
Note that this environment is the global owl environment for an application. The
next section explains how to extend an environment for a specific sub component
and its children.
## Using a sub environment
It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment.
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
```js
class FormComponent extends Component {
constructor(parent, props) {
super(parent, props);
useSubEnv({ myKey: someValue });
}
}
```
## Content of an Environment
Some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({ templates });
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc,
},
debug: false,
inMobileMode: true,
};
}
async function start() {
const env = await myEnv();
mount(App, { target: document.body, env });
}
```
## Special Keys
There are two special key/value added by Owl if not provided in the environment:
the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
+81
View File
@@ -0,0 +1,81 @@
# 🦉 Error Handling 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
## Overview
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
## Example
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
## Reference
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allows us to properly handle the error, and to
not break the application.
There are important things to know:
- If an error that occured in the internal rendering cycle is not caught, then
Owl will destroy the full application. This is done on purpose, because Owl
cannot guarantee that the state is not corrupted from this point on.
- errors coming from event handlers are NOT managed by `catchError` or any other
owl mechanism. This is up to the application developer to properly recover
from an error
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function (error) {
// do something
// react to the error
});
```
@@ -5,9 +5,9 @@ parts of the code. Owl has a very simple bus class, which manages subscriptions,
triggering events, and callbacks. triggering events, and callbacks.
```js ```js
const bus = new owl.EventBus(); const bus = new owl.core.EventBus();
bus.on("some-event", null, function(...args) { bus.on("some-event", null, function (...args) {
console.log(...args); console.log(...args);
}); });
@@ -23,5 +23,3 @@ Its API is:
| `off(eventType, owner)` | remove all listeners for an owner | | `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event | | `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions | | `clear` | remove all subscriptions |
Note that the [`Store`](store.md) is an example of an `EventBus`.
+151
View File
@@ -0,0 +1,151 @@
# 🦉 Event Handling 🦉
## Content
- [Event Handling](#event-handling)
- [Business DOM Events](#business-dom-events)
- [Inline Event Handlers](#inline-event-handlers)
- [Modifiers](#modifiers)
## Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
## Business DOM Events
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<MyComponent t-on-menu-loaded="someMethod" />
```
```js
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind its arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Type Hinting
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
You can then describe the type of the event, so you will see typing errors...
```typescript
this.trigger<MyCustomPayload>("my-custom-event", payload);
```
```typescript
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
```
## Inline Event Handlers
One can also directly specify inline statements. For example,
```xml
<button t-on-click="state.counter++">Increment counter</button>
```
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => {
context.state.counter++;
});
```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
## Modifiers
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click.stop="">Do something</button>
```
This will simply stop the propagation of the event.
+446
View File
@@ -0,0 +1,446 @@
# 🦉 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)
- [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [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, in the _setup_ method, 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 });
}
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
As you can see, the `useState` hook does not need to be given a reference to
the component. This is possible because there is a way to get a reference to the
current component: 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. This is also a good thing for performance reasons (Owl can use
this to optimize its implementation), and for a clean architecture (this makes
it easier for developers to understand what is really happening in a component).
### `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
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Notes:
- if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`,
- on a component, accessing `ref.el` will get the root node of the component.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb_templating_language.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.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### 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() {
const env = useEnv();
return 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.
+133
View File
@@ -0,0 +1,133 @@
# 🦉 Miscellaneous 🦉
## Content
- [Portal](#portal)
- [AsyncRoot](#asyncroot)
## `Portal`
### Overview
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
of DOM to the node represented by its sole `target` props.
This component aims at helping the implementation of the needed infrastructure
for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](slots.md).
This slot must contain only **one** node, which in turn can have as many children as necessary.
The element under which the content will be teleported is represented as a selector
by the `target` props which only accepts a string as value.
The `target` props only supports static selector, and is not meant to be passed to `Portal`
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
By contrast, `<Portal target="state.target" />` is not supported.
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
### Example
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component, mount } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
class App extends Component {
static components = { Portal, TeleportedComponent };
}
mount(App, { target: document.body });
```
XML:
```xml
<templates>
<div t-name="TeleportedComponent">
<span>I will move soon enough</span>
</div>
<div t-name="App">
<span>I am like the rest of us</span>
<Portal target="'body'">
<TeleportedComponent />
</Portal>
</div>
</templates>
```
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
`TeleportedComponent` is acting as a Dialog here.
The resulting DOM will look like:
```xml
<body>
<div>
<span>I am like the rest of us</span>
<portal></portal>
</div>
<div>
<span>I will move soon enough</span>
</div>
</body>
```
### Expected Behaviors
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
Beware that those re-directed events are copies of the original event.
They have:
- The same payload.
- The same `originalComponent` than their original counterpart,
that is the actual Component that triggered it.
- A **different** `target` property than their original counterpart.
The `target` of a re-directed event is necessarily the `Portal`'s root node.
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
up to the `body`.
## `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.
+60
View File
@@ -0,0 +1,60 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+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.core.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.core.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.revNumber(obj.a); // 1
obj.a.b = 2;
observer.revNumber(obj.a); // 2
```
The `revNumber` can also return 0, which indicates that the value is not
observed.
+97
View File
@@ -0,0 +1,97 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Good Practices](#good-practices)
- [Dynamic Props](#dynamic-props)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
- `style` and `class` attributes are excluded as well (they are applied by Owl on
the root element of the component).
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
<ComponentC style="color:red;" class="left-pane" />
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
- for `ComponentC`: empty object
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
+103
View File
@@ -0,0 +1,103 @@
# 🦉 Props Validation 🦉
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [config page](config.md#mode))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
Examples:
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
};
```
+153
View File
@@ -0,0 +1,153 @@
# 🦉 QWeb Engine 🦉
## Content
- [Overview](#overview)
- [Reference](#reference)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. 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`, ...
We present in this section the engine, not the templating language.
## Reference
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](../miscellaneous/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](environment.md). 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).
### Translations
take care of this and "cherry-pick" 8464a1b04e7469434f9dcb3d68a543f58cb61b8e
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.
@@ -1,10 +1,9 @@
# 🦉 QWeb 🦉 # 🦉 QWeb Templating Language🦉
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [Directives](#directives) - [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference) - [Reference](#reference)
- [White Spaces](#white-spaces) - [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes) - [Root Nodes](#root-nodes)
@@ -14,150 +13,78 @@
- [Setting Variables](#setting-variables) - [Setting Variables](#setting-variables)
- [Conditionals](#conditionals) - [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging) - [Debugging](#debugging)
## Overview ## 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 [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 mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML. 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 ```xml
<div> <div>
<span t-if="somecondition">Some string</span> <span t-if="somecondition">Some string</span>
<ul t-else="1"> <ul t-else="">
<li t-foreach="messages" t-as="message"> <li t-foreach="messages" t-as="message">
<t t-esc="message"> <t t-esc="message"/>
</li> </li>
</ul> </ul>
</div> </div>
``` ```
The QWeb class in the OWL project is an implementation of that specification Template directives are specified as XML attributes prefixed with `t-`, for
with a few interesting points: instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
- it compiles templates into functions that output a virtual DOM instead of a To avoid element rendering, a placeholder element `<t>` is also available, which
string. This is necessary for the component system. executes its directive but doesnt generate any output in and of itself.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the templating language, including its Owl specific
extensions.
## Directives ## Directives
We present here a list of all standard QWeb directives: For reference, here is a list of all standard QWeb directives:
| Name | Description | | Name | Description |
| ------------------------------ | ------------------------------------------------------------ | | ------------------------------ | -------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) | | `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) | | `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) | | `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) | | `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) | | `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) | | `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) | | `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) | | `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) | | `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
The component system in Owl requires additional directives, to express various The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives: needs. Here is a list of all Owl specific directives:
| Name | Description | | Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------- | | ------------------------ | ------------------------------------------------------------------------------- |
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) | | `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) | | `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-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](component.md#event-handling) | | `t-on-*` | [Event handling](event_handling.md) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) | | `t-slot` | [Rendering a slot](slots.md) |
| `t-slot` | [Rendering a slot](component.md#slots) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
It's API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial
templates (see `addTemplates` for more information on format of the string).
```js
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`register(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.register("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsability: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference ## 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
White spaces in a templates are handled in a special way: White spaces in a template are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace - consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored - if a whitespace-only text node contains a linebreak, it is ignored
@@ -178,7 +105,7 @@ precisely, the result of a template rendering should have a single root node:
<!–– ok: result has one single root node ––> <!–– ok: result has one single root node ––>
<t> <t>
<div t-if="someCondition">foo</div> <div t-if="someCondition">foo</div>
<span t-else="1">bar</span> <span t-else="">bar</span>
</t> </t>
``` ```
@@ -192,14 +119,14 @@ root nodes.
### Expression Evaluation ### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the 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: component). For example, `a + b.c(d)` will be converted into:
```js ```js
context["a"] + context["b"].c(context["d"]); context["a"] + context["b"].c(context["d"]);
``` ```
It is useful to explain the various rules that applies on these expressions: 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. 1. it should be a simple expression which returns a value. It cannot be a statement.
@@ -225,14 +152,14 @@ It is useful to explain the various rules that applies on these expressions:
3. it can use a few special operators to avoid using symbols such as `<`, `>`, 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. `&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by | | Word | replaced with |
| ----- | ------------------- | | ----- | ------------- |
| `and` | `&&` | | `and` | `&&` |
| `or` | `\|\|` | | `or` | `\|\|` |
| `gt` | `>` | | `gt` | `>` |
| `gte` | `>=` | | `gte` | `>=` |
| `lt` | `<` | | `lt` | `<` |
| `lte` | `<=` | | `lte` | `<=` |
So, one can write this: So, one can write this:
@@ -277,6 +204,11 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
<p><span>foo</span></p> <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 ### 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, ... 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, ...
@@ -376,14 +308,59 @@ If an expression evaluates to a falsy value, it will not be set at all:
<div t-att-foo="false"/> <!-- result: <div></div> --> <div t-att-foo="false"/> <!-- result: <div></div> -->
``` ```
There is another way to format a string attribute: the `t-attf-` directive. With It is sometimes convenient to format an attribute with string interpolation. In
it, you get string interpolation: 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 ```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/> <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> --> <!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
``` ```
If we need completely dynamic attribute names, then there is an additional
directive: `t-att`, which takes either an object (with keys mapping to their
values) or a pair `[key, value]`. For example:
```xml
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops ### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the QWeb has an iteration directive `t-foreach` which take an expression returning the
@@ -418,7 +395,7 @@ is equivalent to the previous example.
or an object (the current item will be the current key). 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 In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced by the name variables for various data points (note: `$as` will be replaced with the name
passed to `t-as`): passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and - `$as_value`: the current iteration value, identical to `$as` for lists and
@@ -436,19 +413,94 @@ the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context. into the global context.
```xml ```xml
<t t-set="existing_variable" t-value="False"/> <t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False --> <!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i"> <p t-foreach="Array(3)" t-as="i">
<t t-set="existing_variable" t-value="True"/> <t t-set="existing_variable" t-value="true"/>
<t t-set="new_variable" t-value="True"/> <t t-set="new_variable" t-value="true"/>
<!-- existing_variable and new_variable now True --> <!-- existing_variable and new_variable now true -->
</p> </p>
<!-- existing_variable always True --> <!-- existing_variable always true -->
<!-- new_variable undefined --> <!-- new_variable undefined -->
``` ```
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
If there is no `t-key` directive, Owl will use the index as a default key.
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:
```xml
<t t-foreach="...items" t-as="item">...</t>
```
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Rendering Sub Templates ### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used QWeb templates can be used for top level rendering, but they can also be used
@@ -497,6 +549,46 @@ will result in :
</div> </div>
``` ```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
### Dynamic sub templates
The `t-call` directive can also be used to dynamically call a sub template,
using string interpolation. For example:
```xml
<div t-name="main-template">
<t t-call="{{template}}">
<em>content</em>
</t>
</div>
```
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
is not wanted, there is a `t-translation` directive which can turn off
translations (if it is set to the `off` value), with the following rules:
- 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`.
See [here](qweb_engine.md#translations) for more information on how to setup a
translate function in Owl QWeb.
### Debugging ### Debugging
The javascript QWeb implementation provides two useful debugging directives: The javascript QWeb implementation provides two useful debugging directives:
@@ -505,7 +597,7 @@ The javascript QWeb implementation provides two useful debugging directives:
```xml ```xml
<t t-if="a_test"> <t t-if="a_test">
<t t-debug=""> <t t-debug=""/>
</t> </t>
``` ```
@@ -518,4 +610,4 @@ will stop execution if the browser dev tools are open.
<t t-log="foo"/> <t t-log="foo"/>
``` ```
will print 42 to the console will print 42 to the console.
+111
View File
@@ -0,0 +1,111 @@
# 🦉 Slots 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
## Overview
Owl is a template based component system. There is therefore a need to be able
to make generic components. For example, imagine a generic `Dialog`
component, which is able to display some arbitrary content.
Obviously, we want to use this component everywhere in our application, to
display various different content. The `Dialog` component is technically the
owner of its content, but is only a container. The user of the `Dialog` is
the component that want to _inject_ something inside the `Dialog`. This is
exactly what slots are for.
## Example
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
Slots are inserted with the `t-slot` directive:
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
<div class="modal-content">
<t t-slot="content"/>
</div>
<div class="modal-footer">
<t t-slot="footer"/>
</div>
</div>
```
Slots are defined by the caller, with the `t-set-slot` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="'Some Dialog'">
<t t-set-slot="content">
<div>hey</div>
</t>
<t t-set-slot="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
</div>
```
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Note: Owl previously used the `t-set` directive to define the content of a slot.
This is deprecated and should no longer be used in new code.
## Reference
### Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
### Default content
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+184
View File
@@ -0,0 +1,184 @@
# 🦉 Tags 🦉
## Content
- [Overview](#overview)
- [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../learning/how_to_write_sfc.md).
## XML tag
The `xml` tag is certainly the most useful tag. It is used to define an inline
QWeb template for a component. 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>
`;
...
}
```
## CSS tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
<div class="my-component">some template</div>
`;
static style = css`
.my-component {
color: red;
}
`;
}
```
The `css` tag registers internally the css information. Then, whenever the first
instance of the component is created, will add a `<style>` tag to the document
`<head>`.
Note that to make it more useful, like other css preprocessors, the `css` tag
accepts a small extension of the css specification: css scopes can be nested,
and the rules will then be expanded by the `css` helper:
```scss
.my-component {
display: block;
.sub-component h {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component .sub-component h {
color: red;
}
```
This extension brings another useful feature: the `&` selector which refers to
the parent selector. For example, we want our component to be red when hovered.
We would like to write something like:
```scss
.my-component {
display: block;
:hover {
color: red;
}
}
```
but it will be formatted as:
```css
.my-component {
display: block;
}
.my-component :hover {
color: red;
}
```
The `&` selector can be used to solve this problem:
```scss
.my-component {
display: block;
&:hover {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component:hover {
color: red;
}
```
Now, there is no additional processing done by the `css` tag. However, since it
is done in javascript at runtime, we actually have more power. For example:
1. sharing values between javascript and css:
```js
import { theme } from "./theme";
class MyComponent extends Component {
static template = xml`<div class="my-component">...</div>`;
static style = css`
.my-component {
color: ${theme.MAIN_COLOR};
background-color: ${theme.SECONDARY_color};
}
`;
}
```
2. scoping rules to the current component:
```js
import { generateUUID } from "./utils";
const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
}
```
+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 env = { qweb };
await mount(App, { env, target: document.body });
});
```
or alternatively:
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const env = { qweb };
await mount(App, { env, target: 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);
```
-172
View File
@@ -1,172 +0,0 @@
# 🦉 Router 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [`t-routecomponent`](#t-routecomponent)
- [Link](#link)
## Overview
It is often useful to organize an application around urls. If the application is
a single page application, then we need a way to manage those urls in the browser.
This is why there are many different routers for different frameworks. A generic
router can do the job just fine, but a specialized router for Owl can give a
better developer experience.
The Owl router support the following features:
- `history` or `hash` mode
- declarative routes
- route redirection
- navigation guards
- parameterized routes
- a `<Link>` component
- a `t-routecomponent` directive
Note that it is still in early stage of developments, and there are probably
still some issues.
## Example
To use the Owl router, there are some steps that needs to be done:
- declare some routes
- create a router
- add it to the environment
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
export const ROUTES = [
{ name: "LANDING", path: "/", component: Landing },
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
{ name: "SIGN_UP", path: "/signup", component: SignUp },
{ name: "SIGN_IN", path: "/signin", component: SignIn },
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
];
function makeEnvironment() {
...
const env = { qweb };
env.session = new Session(env);
env.router = new owl.router.Router(env, ROUTES);
await env.router.start();
return env;
}
```
Notice that the router needs to be started. This is an asynchronous operation
because it needs to apply the potential navigation guards on the current route
(which may or may not mean that the application is redirected to another route).
## Reference
### Route definition
A route need to be defined as an object with the following keys:
- `name` (optional): a (unique) string useful to identify the current route. If not
given, it will be assigned an automatic name,
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
It also can be `*`, to catch all remaining routes.
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
directive if the route is active
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
### `Router`
The `Router` constructor takes three arguments:
- `env`: a valid environment,
- a list of routes,
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
```
Note that the route are defined in a list, and the order matters: the router
tries to find a match by going down the list.
The router needs to be added to the environment in the `router` sub key.
Once a router is created, it needs to be started. This is necessary to initialize
its current state to the current URL (and also, to potentially apply any
navigation guards and/or redirecting).
```js
await router.start();
```
Once started, the router will keep track of the current url and reflect its
value in two keys:
- `router.currentRoute`
- `router.currentParams`
The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url):
```js
router.navigate({to: 'USER', params: {id: 51}});
```
### Navigation Guards
Navigation guards are very useful to be able to execute some business logic/
perform some actions or redirect to other routes whenever the application is
entering a new route. For example, the following guard checks if there is an
authenticated user, and if it is not the case, redirect to the sign in route.
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
```
A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object.
### `t-routecomponent`
The `t-routecomponent` directive directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<t t-routecomponent="1"/>
</div>
```
### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any
content. It will compute the proper href from its props, and allow Owl to
properly navigate to a given url if clicked on it.
```xml
<Link to="'HOME'">Home</Link>
```
-289
View File
@@ -1,289 +0,0 @@
# 🦉 Store 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
Managing the state in an application is not an easy task. In some cases, the
state of an application can be part of the component tree, in a natural way.
However, there are situations where some part of the state need to be displayed
in various parts of the user interface, and then, it is not obvious which
component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some state, and let the developer update it in a structured way, with `actions`.
Owl components can then connect to the store, and will be updated if necessary.
Note: Owl store is inspired by React Redux and VueX.
## Example
Here is what a simple store looks like:
```js
const actions = {
addTodo({ state }, message) {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false
});
}
};
const state = {
todos: [],
nextId: 1
};
const store = new owl.Store({ state, actions });
store.on("update", () => console.log(store.state));
// updating the state
store.dispatch("addTodo", "fix all bugs");
```
This example shows how a store can be defined and used. Note that in most cases,
actions will be dispatched by connected components.
## Reference
### `Store`
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
whenever its state is changed. Note that these events are triggered only after a
microtask tick, so only one event will be triggered for any number of state changes in a
call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
The `Store` class is quite small. It has two public methods:
- its constructor
- `dispatch`
The constructor takes a configuration object with four (optional) keys:
- the initial state
- the actions
- the getters
- the environment
```javascript
const config = {
state,
actions,
getters,
env
};
const store = new Store(config);
```
### Actions
Actions are used to coordinate state changes. It can be used for both synchronous
and asynchronous logic.
```js
const actions = {
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo;
} catch {
state.loginState = "error";
}
}
};
```
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partial corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
}
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
}
};
```
### Getters
Usually, data contained in the store will be stored in a normalized way. For
example,
```js
{
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
authors: [{id: 4, name: 'John'}]
}
```
However, the user interface will probably need some denormalized data like
```js
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
```
This is what `getters` are for: they give a centralized way to process and
transform the data contained in the store.
```js
const getters = {
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => (a.id = post.id));
return {
id,
author,
content: post.content
};
}
};
// somewhere else
const post = store.getters.getPost(id);
```
Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument
is a string or a number.
### Connecting a Component
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The `ConnectedComponent` class can be configured with the following fields:
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict.
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)
Note that the class `ConnectedComponent` has a `dispatch` method. This means
that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
</button>
```
### Semantics
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children)
- they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
-58
View File
@@ -1,58 +0,0 @@
# 🦉 Tooling 🦉
## Content
- [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
## Overview
To help work with/improve/learn OWL, there are a few extras tools/settings.
- development mode: enable better error reporting for the developer
- a playground application: a space to experiment and learn Owl.
- a benchmarks application: allow comparison with a few common frameworks
The two applications are available in the `tools/` folder, and can be accessed
by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
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.
-65
View File
@@ -1,65 +0,0 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadTemplates`](#loadtemplates): loading xml files
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
templates
]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
});
```
```js
owl.utils.whenReady(function() {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
});
```
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
a promise, so the caller can properly react when it is ready. Also, it is smart:
it maintains a list of urls previously loaded (or currently being loaded), and
prevent doing twice the work.
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
## `loadTemplates`
```js
async function makeEnv() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
return { qweb };
}
```
## `escape`
## `debounce`
+43 -21
View File
@@ -1,53 +1,74 @@
{ {
"name": "owl", "name": "@odoo/owl",
"version": "0.19.0", "version": "2.0.0-alpha1",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
"module": "dist/owl.es.js",
"types": "dist/types/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=12.18.3"
},
"scripts": { "scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c", "build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle", "build": "npm run build:bundle",
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest", "test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py", "playground:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve", "playground": "npm run build && npm run playground:serve",
"pretools:watch": "npm run build", "preplayground:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"" "playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"publish": "npm run build && npm publish",
"release": "node tools/release.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/odoo/owl.git" "url": "git+https://github.com/odoo/owl.git"
}, },
"author": "Odoo", "author": "Odoo",
"license": "GPL-3.0-or-later", "license": "LGPL-3.0-only",
"bugs": { "bugs": {
"url": "https://github.com/odoo/owl/issues" "url": "https://github.com/odoo/owl/issues"
}, },
"homepage": "https://github.com/odoo/owl#readme", "homepage": "https://github.com/odoo/owl#readme",
"devDependencies": { "devDependencies": {
"@types/jest": "^23.3.12", "@types/jest": "^27.0.1",
"@types/node": "^14.11.8",
"chalk": "^3.0.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
"jest": "^23.6.0", "github-api": "^3.3.0",
"jest-environment-jsdom": "^24.7.1", "jest": "^27.1.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rollup": "^1.6.0", "prettier": "2.4.1",
"rollup-plugin-typescript2": "^0.20.1", "rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.1",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^27.0.5",
"typescript": "^3.2.2", "typescript": "4.5.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"dependencies": {},
"jest": { "jest": {
"testEnvironment": "jsdom",
"roots": [ "roots": [
"<rootDir>/src", "<rootDir>/src",
"<rootDir>/tests" "<rootDir>/tests"
], ],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": { "transform": {
"^.+\\.ts?$": "ts-jest" "^.+\\.ts?$": "ts-jest"
}, },
@@ -63,6 +84,7 @@
] ]
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100,
"endOfLine": "auto"
} }
} }
+28
View File
@@ -0,0 +1,28 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.4.7
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### 1.x
- add chrome and firefox devtools,
- fix every bugs,
- improve documentation,
- small backward compatible improvements.
### 2.x (2020? 2021? 2022?)
- stop support for `t-set` directive to define the content of a slot
Maybe:
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
much faster
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations.
+67 -10
View File
@@ -1,14 +1,71 @@
import { version } from "./package.json"; import pkg from "./package.json";
import git from "git-rev-sync"; import git from "git-rev-sync";
import typescript from 'rollup-plugin-typescript2';
import { terser } from "rollup-plugin-terser";
const name = "owl";
const extend = true;
/**
* Meta data to be added on the __info__ object.
* Used to let external tools know the current owl version.
*/
const outro = `
__info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}';
__info__.hash = '${git.short()}';
__info__.url = 'https://github.com/odoo/owl';
`;
/**
* Generate from a string depicting a path a new path for the minified version.
* @param {string} pkgFileName file name
*/
function generateMinifiedNameFromPkgName(pkgFileName) {
const parts = pkgFileName.split('.');
parts.splice(parts.length - 1, 0, "min");
return parts.join('.');
}
/**
* Get the rollup config based on the arguments
* @param {string} format format of the bundle
* @param {string} generatedFileName generated file name
* @param {boolean} minified should it be minified
*/
function getConfigForFormat(format, generatedFileName, minified = false) {
return {
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
format: format,
name: name,
extend: extend,
outro: outro,
freeze: false,
plugins: minified ? [terser()] : [],
indent: ' ', // indent with 4 spaces
};
}
// rollup.config.js
export default { export default {
input: "dist/owl/index.js", input: "src/index.ts",
output: { output: [
file: "dist/owl.js",
format: "iife", /**
name: "owl", * Read about module formats:
extend: true, * https://auth0.com/blog/javascript-module-systems-showdown/
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';` * https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
} */
getConfigForFormat('esm', pkg.module),
getConfigForFormat('esm', pkg.module, true),
getConfigForFormat('cjs', pkg.main),
getConfigForFormat('cjs', pkg.main, true),
getConfigForFormat('iife', pkg.browser),
getConfigForFormat('iife', pkg.browser, true),
],
plugins: [
typescript({
useTsconfigDeclarationDir: true
}),
]
}; };
+105
View File
@@ -0,0 +1,105 @@
import { Component } from "../component/component";
import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { nodeErrorHandlers } from "../component/error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export interface Env {
[key: string]: any;
}
export interface AppConfig extends TemplateSetConfig {
env?: Env;
props?: any;
}
export const DEV_MSG = `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
export class App<T extends typeof Component = any> extends TemplateSet {
Root: T;
props: any;
env: Env;
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
root: ComponentNode | null = null;
constructor(Root: T, config: AppConfig = {}) {
super(config);
this.Root = Root;
if (config.dev) {
console.info(DEV_MSG);
}
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
this.env = Object.freeze(Object.defineProperties({}, descrs));
this.props = config.props || {};
}
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
this.checkTarget(target);
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
}
checkTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
}
}
makeNode(Component: T, props: any): ComponentNode {
return new ComponentNode(Component, props, this);
}
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
const promise: any = new Promise((resolve, reject) => {
let isResolved = false;
// manually set a onMounted callback.
// that way, we are independant from the current node.
node.mounted.push(() => {
resolve(node.component);
isResolved = true;
});
// Manually add the last resort error handler on the node
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.unshift((e) => {
if (isResolved) {
console.error(e);
} else {
reject(e);
}
throw e;
});
});
node.mountComponent(target, options);
return promise;
}
destroy() {
if (this.root) {
this.root.destroy();
}
}
}
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement,
config: AppConfig & MountOptions = {}
): Promise<InstanceType<T>> {
return new App(C, config).mount(target, config);
}
+213
View File
@@ -0,0 +1,213 @@
import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
import { VPortal } from "../portal";
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
*/
function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callPortal(
ctx: any,
parent: any,
key: string,
target: string,
content: (ctx: any, node: any, key: string) => BDom
): BDom {
const portal = new VPortal(target, content(ctx, parent, key), ctx.__owl__) as any;
return portal;
}
function callSlot(
ctx: any,
parent: any,
key: string,
name: string,
dynamic: boolean,
extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
const slots = (ctx.props && ctx.props.slots) || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = Object.create(__ctx || {});
if (__scope) {
slotScope[__scope] = extra || {};
}
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
if (defaultContent) {
let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined;
if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else {
child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
}
return multi([child1, child2]);
}
return slotBDom || text("");
}
function capture(ctx: any): any {
const component = ctx.__owl__.component;
const result = Object.create(component);
for (let k in ctx) {
result[k] = ctx[k];
}
return result;
}
function withKey(elem: any, k: string) {
elem.key = k;
return elem;
}
function prepareList(collection: any): [any[], any[], number, any[]] {
let keys: any[];
let values: any[];
if (Array.isArray(collection)) {
keys = collection;
values = collection;
} else if (collection) {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
throw new Error("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
}
const isBoundary = Symbol("isBoundary");
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
const ctx0 = ctx;
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
const newCtx = ctx.__proto__;
if (!newCtx) {
ctx = ctx0;
break;
}
ctx = newCtx;
}
ctx[key] = value;
}
function toNumber(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
}
function shallowEqual(l1: any[], l2: any[]): boolean {
for (let i = 0, l = l1.length; i < l; i++) {
if (l1[i] !== l2[i]) {
return false;
}
}
return true;
}
class LazyValue {
fn: any;
ctx: any;
node: any;
constructor(fn: any, ctx: any, node: any) {
this.fn = fn;
this.ctx = capture(ctx);
this.node = node;
}
evaluate(): any {
return this.fn(this.ctx, this.node);
}
toString() {
return this.evaluate().toString();
}
}
/*
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any): ReturnType<typeof toggler> {
if (!value) {
return value;
}
let safeKey;
let block;
if (value instanceof Markup) {
safeKey = `string_safe`;
block = html(value as string);
} else if (value instanceof LazyValue) {
safeKey = `lazy_value`;
block = value.evaluate();
} else if (typeof value === "string") {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
return toggler(safeKey, block);
}
let boundFunctions = new WeakMap();
function bind(ctx: any, fn: Function): Function {
let component = ctx.__owl__.component;
let boundFnMap = boundFunctions.get(component);
if (!boundFnMap) {
boundFnMap = new WeakMap();
boundFunctions.set(component, boundFnMap);
}
let boundFn = boundFnMap.get(fn);
if (!boundFn) {
boundFn = fn.bind(component);
boundFnMap.set(fn, boundFn);
}
return boundFn;
}
type RefMap = { [key: string]: HTMLElement | null };
type RefSetter = (el: HTMLElement | null) => void;
function multiRefSetter(refs: RefMap, name: string): RefSetter {
let count = 0;
return (el) => {
if (el) {
count++;
if (count > 1) {
throw new Error("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
refs[name] = el;
}
};
}
export const UTILS = {
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
callPortal,
capture,
withKey,
prepareList,
setContextValue,
multiRefSetter,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
bind,
};
+126
View File
@@ -0,0 +1,126 @@
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
import { compile, Template } from "../compiler";
import { component } from "../component/component_node";
import { UTILS } from "./template_helpers";
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
export const globalTemplates: { [key: string]: string | Node } = {};
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc;
}
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document;
}
export class TemplateSet {
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
utils: typeof UTILS = Object.assign({}, UTILS, {
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
},
getTemplate: (name: string) => this.getTemplate(name),
});
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
this.translateFn = config.translateFn;
this.translatableAttributes = config.translatableAttributes;
if (config.templates) {
this.addTemplates(config.templates);
}
}
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
if (name in this.rawTemplates && !options.allowDuplicate) {
throw new Error(`Template ${name} already defined`);
}
this.rawTemplates[name] = template;
}
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
if (!xml) {
// empty string
return;
}
xml = xml instanceof Document ? xml : parseXML(xml);
for (const template of xml.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name")!;
template.removeAttribute("t-name");
this.addTemplate(name, template, options);
}
}
getTemplate(name: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`);
}
const templateFn = this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
this.templates[name] = (context, parent) => this.templates[name](context, parent);
const template = templateFn(bdom, this.utils);
this.templates[name] = template;
}
return this.templates[name];
}
_compileTemplate(name: string, template: string | Node) {
return compile(template, {
name,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
}
}
// -----------------------------------------------------------------------------
// xml tag helper
// -----------------------------------------------------------------------------
export function xml(...args: Parameters<typeof String.raw>) {
const name = `__template__${xml.nextId++}`;
const value = String.raw(...args);
globalTemplates[name] = value;
return name;
}
xml.nextId = 1;
+167
View File
@@ -0,0 +1,167 @@
import type { Setter } from "./block_compiler";
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
const tokenList = DOMTokenList.prototype;
const tokenListAdd = tokenList.add;
const tokenListRemove = tokenList.remove;
const isArray = Array.isArray;
const { split, trim } = String.prototype;
const wordRegexp = /\s+/;
/**
* We regroup here all code related to updating attributes in a very loose sense:
* attributes, properties and classs are all managed by the functions in this
* file.
*/
function setAttribute(this: HTMLElement, key: string, value: any) {
switch (value) {
case false:
case undefined:
removeAttribute.call(this, key);
break;
case true:
elemSetAttribute.call(this, key, "");
break;
default:
elemSetAttribute.call(this, key, value);
}
}
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
return function (this: HTMLElement, value: any) {
setAttribute.call(this, attr, value);
};
}
export function attrsSetter(this: HTMLElement, attrs: any) {
if (isArray(attrs)) {
setAttribute.call(this, attrs[0], attrs[1]);
} else {
for (let k in attrs) {
setAttribute.call(this, k, attrs[k]);
}
}
}
export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
if (isArray(attrs)) {
const name = attrs[0];
const val = attrs[1];
if (name === oldAttrs[0]) {
if (val === oldAttrs[1]) {
return;
}
setAttribute.call(this, name, val);
} else {
removeAttribute.call(this, oldAttrs[0]);
setAttribute.call(this, name, val);
}
} else {
for (let k in oldAttrs) {
if (!(k in attrs)) {
removeAttribute.call(this, k);
}
}
for (let k in attrs) {
const val = attrs[k];
if (val !== oldAttrs[k]) {
setAttribute.call(this, k, val);
}
}
}
}
function toClassObj(expr: string | number | { [c: string]: any }) {
const result: { [c: string]: any } = {};
switch (typeof expr) {
case "string":
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
const str = trim.call(expr);
if (!str) {
return {};
}
let words = split.call(str, wordRegexp);
for (let i = 0, l = words.length; i < l; i++) {
result[words[i]] = true;
}
return result;
case "object":
// this is already an object but we may need to split keys:
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
for (let key in expr as any) {
const value = (expr as any)[key];
if (value) {
const words = split.call(key, wordRegexp);
for (let word of words) {
result[word] = value;
}
}
}
return result;
case "undefined":
return {};
case "number":
return { [expr as number]: true };
default:
return { [expr as any]: true };
}
}
export function setClass(this: HTMLElement, val: any) {
val = val === "" ? {} : toClassObj(val);
// add classes
const cl = this.classList;
for (let c in val) {
tokenListAdd.call(cl, c);
}
}
export function updateClass(this: HTMLElement, val: any, oldVal: any) {
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
val = val === "" ? {} : toClassObj(val);
const cl = this.classList;
// remove classes
for (let c in oldVal) {
if (!(c in val)) {
tokenListRemove.call(cl, c);
}
}
// add classes
for (let c in val) {
if (!(c in oldVal)) {
tokenListAdd.call(cl, c);
}
}
}
export function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) {
(this as any)[name] = value;
};
}
export function isProp(tag: string, key: string): boolean {
switch (tag) {
case "input":
return (
key === "checked" ||
key === "indeterminate" ||
key === "value" ||
key === "readonly" ||
key === "disabled"
);
case "option":
return key === "selected" || key === "disabled";
case "textarea":
return key === "value" || key === "readonly" || key === "disabled";
case "select":
return key === "value" || key === "disabled";
case "button":
case "optgroup":
return key === "disabled";
}
return false;
}
+613
View File
@@ -0,0 +1,613 @@
import {
attrsSetter,
attrsUpdater,
createAttrUpdater,
isProp,
makePropSetter,
setClass,
updateClass,
} from "./attributes";
import { config } from "./config";
import { createEventHandler } from "./events";
import type { VNode } from "./index";
import { VMulti } from "./multi";
import { toText } from "./text";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const elementProto = Element.prototype;
const characterDataProto = CharacterData.prototype;
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeGetFirstChild = getDescriptor(nodeProto, "firstChild").get!;
const nodeGetNextSibling = getDescriptor(nodeProto, "nextSibling").get!;
const NO_OP = () => {};
// -----------------------------------------------------------------------------
// Main compiler code
// -----------------------------------------------------------------------------
type BlockType = (data?: any[], children?: VNode[]) => VNode;
const cache: { [key: string]: BlockType } = {};
/**
* Compiling blocks is a multi-step process:
*
* 1. build an IntermediateTree from the HTML element. This intermediate tree
* is a binary tree structure that encode dynamic info sub nodes, and the
* path required to reach them
* 2. process the tree to build a block context, which is an object that aggregate
* all dynamic info in a list, and also, all ref indexes.
* 3. process the context to build appropriate builder/setter functions
* 4. make a dynamic block class, which will efficiently collect references and
* create/update dynamic locations/children
*
* @param str
* @returns a new block type, that can build concrete blocks
*/
export function createBlock(str: string, deepRemove: boolean = false): BlockType {
if (str in cache) {
return cache[str];
}
// step 0: prepare html base element
const doc = new DOMParser().parseFromString(`<t>${str}</t>`, "text/xml");
const node = doc.firstChild!.firstChild!;
if (config.shouldNormalizeDom) {
normalizeNode(node as any);
}
// step 1: prepare intermediate tree
const tree = buildTree(node);
// step 2: prepare block context
const context = buildContext(tree);
// step 3: build the final block class
const template = tree.el as HTMLElement;
const Block = buildBlock(template, context, deepRemove);
cache[str] = Block;
return Block;
}
// -----------------------------------------------------------------------------
// Helper
// -----------------------------------------------------------------------------
function normalizeNode(node: HTMLElement | Text) {
if (node.nodeType === Node.TEXT_NODE) {
if (!/\S/.test((node as Text).textContent!)) {
(node as Text).remove();
return;
}
}
if (node.nodeType === Node.ELEMENT_NODE) {
if ((node as HTMLElement).tagName === "pre") {
return;
}
}
for (let i = node.childNodes.length - 1; i >= 0; --i) {
normalizeNode(node.childNodes.item(i) as any);
}
}
// -----------------------------------------------------------------------------
// building a intermediate tree
// -----------------------------------------------------------------------------
interface DynamicInfo {
idx: number;
refIdx?: number;
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
isOnlyChild?: boolean;
name?: string;
tag?: string;
event?: string;
}
interface IntermediateTree {
parent: IntermediateTree | null;
firstChild: IntermediateTree | null;
nextSibling: IntermediateTree | null;
el: Node;
info: DynamicInfo[];
isRef?: boolean;
refIdx?: number;
refN: number;
currentNS: string | null;
}
function buildTree(
node: Node,
parent: IntermediateTree | null = null,
domParentTree: IntermediateTree | null = null
): IntermediateTree {
switch (node.nodeType) {
case Node.ELEMENT_NODE: {
// HTMLElement
let currentNS = domParentTree && domParentTree.currentNS;
const tagName = (node as Element).tagName;
let el: Node | undefined = undefined;
const info: DynamicInfo[] = [];
if (tagName.startsWith("block-text-")) {
const index = parseInt(tagName.slice(11), 10);
info.push({ type: "text", idx: index });
el = document.createTextNode("");
}
if (tagName.startsWith("block-child-")) {
if (!domParentTree!.isRef) {
addRef(domParentTree!);
}
const index = parseInt(tagName.slice(12), 10);
info.push({ type: "child", idx: index });
el = document.createTextNode("");
}
const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
}
if (!el) {
el = currentNS
? document.createElementNS(currentNS, tagName)
: document.createElement(tagName);
}
if (el instanceof Element) {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
const attrValue = attrs[i].value;
if (attrName.startsWith("block-handler-")) {
const idx = parseInt(attrName.slice(14), 10);
info.push({
type: "handler",
idx,
event: attrValue,
});
} else if (attrName.startsWith("block-attribute-")) {
const idx = parseInt(attrName.slice(16), 10);
info.push({
type: "attribute",
idx,
name: attrValue,
tag: tagName,
});
} else if (attrName === "block-attributes") {
info.push({
type: "attributes",
idx: parseInt(attrValue, 10),
});
} else if (attrName === "block-ref") {
info.push({
type: "ref",
idx: parseInt(attrValue, 10),
});
} else {
el.setAttribute(attrs[i].name, attrValue);
}
}
}
const tree: IntermediateTree = {
parent,
firstChild: null,
nextSibling: null,
el,
info,
refN: 0,
currentNS,
};
if (node.firstChild) {
const childNode = node.childNodes[0];
if (
node.childNodes.length === 1 &&
childNode.nodeType === Node.ELEMENT_NODE &&
(childNode as Element).tagName.startsWith("block-child-")
) {
const tagName = (childNode as Element).tagName;
const index = parseInt(tagName.slice(12), 10);
info.push({ idx: index, type: "child", isOnlyChild: true });
} else {
tree.firstChild = buildTree(node.firstChild, tree, tree);
el.appendChild(tree.firstChild.el);
let curNode: Node | null = node.firstChild;
let curTree: IntermediateTree | null = tree.firstChild;
while ((curNode = curNode.nextSibling)) {
curTree.nextSibling = buildTree(curNode, curTree, tree);
el.appendChild(curTree.nextSibling.el);
curTree = curTree.nextSibling;
}
}
}
if (tree.info.length) {
addRef(tree);
}
return tree;
}
case Node.TEXT_NODE:
case Node.COMMENT_NODE: {
// text node or comment node
const el =
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!);
return {
parent: parent,
firstChild: null,
nextSibling: null,
el,
info: [],
refN: 0,
currentNS: null,
};
}
}
throw new Error("boom");
}
function addRef(tree: IntermediateTree) {
tree.isRef = true;
do {
tree.refN++;
} while ((tree = tree.parent as any));
}
function parentTree(tree: IntermediateTree): IntermediateTree | null {
let parent = tree.parent;
while (parent && parent.nextSibling === tree) {
tree = parent;
parent = parent.parent;
}
return parent;
}
// -----------------------------------------------------------------------------
// Building a block context
// -----------------------------------------------------------------------------
interface RefCollector {
idx: number;
prevIdx: number;
getVal: Function;
}
export type Setter<T = any> = (this: T, value: any) => void;
export type Updater<T = any> = (this: T, value: any, oldVal: any) => void;
interface Location {
refIdx: number;
setData: Setter;
updateData: Updater;
}
interface IndexedLocation extends Location {
idx: number;
}
interface Child {
parentRefIdx: number;
afterRefIdx?: number;
isOnlyChild?: boolean;
}
interface BlockCtx {
refN: number;
collectors: RefCollector[];
locations: IndexedLocation[];
children: Child[];
cbRefs: number[];
}
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
if (!ctx) {
const children = new Array(tree.info.filter((v) => v.type === "child").length);
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
fromIdx = 0;
}
if (tree.refN) {
const initialIdx = fromIdx!;
const isRef = tree.isRef;
const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
//node
if (isRef) {
for (let info of tree.info) {
info.refIdx = initialIdx!;
}
tree.refIdx = initialIdx!;
updateCtx(ctx, tree);
fromIdx!++;
}
// right
if (nextSibling) {
const idx = fromIdx! + firstChild;
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
buildContext(tree.nextSibling!, ctx, idx);
}
// left
if (firstChild) {
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
buildContext(tree.firstChild!, ctx, fromIdx!);
}
}
return ctx;
}
function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
for (let info of tree.info) {
switch (info.type) {
case "text":
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setText,
updateData: setText,
});
break;
case "child":
if (info.isOnlyChild) {
// tree is the parentnode here
ctx.children[info.idx] = {
parentRefIdx: info.refIdx!,
isOnlyChild: true,
};
} else {
// tree is the anchor text node
ctx.children[info.idx] = {
parentRefIdx: parentTree(tree)!.refIdx!,
afterRefIdx: info.refIdx!,
};
}
break;
case "attribute": {
const refIdx = info.refIdx!;
let updater: any;
let setter: any;
if (isProp(info.tag!, info.name!)) {
const setProp = makePropSetter(info.name!);
setter = setProp;
updater = setProp;
} else if (info.name === "class") {
setter = setClass;
updater = updateClass;
} else {
setter = createAttrUpdater(info.name!);
updater = setter;
}
ctx.locations.push({
idx: info.idx,
refIdx,
setData: setter,
updateData: updater,
});
break;
}
case "attributes":
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: attrsSetter,
updateData: attrsUpdater,
});
break;
case "handler": {
const { setup, update } = createEventHandler(info.event!);
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setup,
updateData: update,
});
break;
}
case "ref":
ctx.cbRefs.push(info.idx);
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setRef,
updateData: NO_OP,
});
}
}
}
// -----------------------------------------------------------------------------
// building the concrete block class
// -----------------------------------------------------------------------------
function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean): BlockType {
let B = createBlockClass(template, ctx);
if (ctx.cbRefs.length) {
const refs = ctx.cbRefs;
B = class extends B {
remove() {
super.remove();
for (let ref of refs) {
let fn = (this as any).data[ref];
fn(null);
}
}
};
}
if (ctx.children.length) {
B = class extends B {
children: (VNode | undefined)[] | undefined;
constructor(data?: any[], children?: VNode[]) {
super(data);
this.children = children;
}
};
B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
if (deepRemove) {
const blockRemove = B.prototype.remove;
const vMultiRemove = VMulti.prototype.remove;
B.prototype.remove = function () {
blockRemove.call(this);
vMultiRemove.call(this);
};
}
return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children);
}
return (data?: any[]) => new B(data);
}
type Constructor<T> = new (...args: any[]) => T;
type BlockClass = Constructor<VNode<any>>;
function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
const { refN, collectors, children } = ctx;
const colN = collectors.length;
ctx.locations.sort((a, b) => a.idx - b.idx);
const locations: Location[] = ctx.locations.map((loc) => ({
refIdx: loc.refIdx,
setData: loc.setData,
updateData: loc.updateData,
}));
const locN = locations.length;
const childN = children.length;
const childrenLocs = children;
const isDynamic = refN > 0;
// these values are defined here to make them faster to lookup in the class
// block scope
const nodeCloneNode = nodeProto.cloneNode;
const nodeInsertBefore = nodeProto.insertBefore;
const elementRemove = elementProto.remove;
return class Block {
el: HTMLElement | undefined;
refs: Node[] | undefined;
data: any[] | undefined;
parentEl?: HTMLElement | undefined;
children?: (VNode | undefined)[];
constructor(data?: any[]) {
this.data = data;
}
beforeRemove() {}
remove() {
console.log('ddd')
elementRemove.call(this.el);
}
firstNode(): Node {
return this.el!;
}
moveBefore(other: Block | null, afterNode: Node | null) {
const target = other ? other.el! : afterNode;
nodeInsertBefore.call(this.parentEl, this.el!, target);
}
mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true);
nodeInsertBefore.call(parent, el, afterNode);
if (isDynamic) {
// collecting references
const refs: Node[] = new Array(refN);
this.refs = refs;
refs[0] = el;
for (let i = 0; i < colN; i++) {
const w = collectors[i];
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
}
// applying data to all update points
if (locN) {
const data = this.data!;
for (let i = 0; i < locN; i++) {
const loc = locations[i];
loc.setData.call(refs[loc.refIdx], data[i]);
}
}
// preparing all children
if (childN) {
const children = this.children;
for (let i = 0; i < childN; i++) {
const child = children![i];
if (child) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child.isOnlyChild = loc.isOnlyChild;
child.mount(refs[loc.parentRefIdx] as any, afterNode);
}
}
}
}
this.el = el as HTMLElement;
this.parentEl = parent;
}
patch(other: Block, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const refs = this.refs!;
// update texts/attributes/
if (locN) {
const data1 = this.data!;
const data2 = other.data!;
for (let i = 0; i < locN; i++) {
const val1 = data1[i];
const val2 = data2[i];
if (val1 !== val2) {
const loc = locations[i];
loc.updateData.call(refs[loc.refIdx], val2, val1);
}
}
this.data = data2;
}
// update children
if (childN) {
let children1 = this.children;
const children2 = other.children;
for (let i = 0; i < childN; i++) {
const child1 = children1![i];
const child2 = children2![i];
if (child1) {
if (child2) {
child1.patch(child2, withBeforeRemove);
} else {
if (withBeforeRemove) {
child1.beforeRemove();
}
child1.remove();
children1![i] = undefined;
}
} else if (child2) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child2.mount(refs[loc.parentRefIdx] as any, afterNode);
children1![i] = child2;
}
}
}
}
toString() {
const div = document.createElement("div");
this.mount(div, null);
return div.innerHTML;
}
};
}
function setText(this: Text, value: any) {
characterDataSetData.call(this, toText(value));
}
function setRef(this: HTMLElement, fn: any) {
fn(this);
}
+28
View File
@@ -0,0 +1,28 @@
export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[]; data: any[] } {
dataList = dataList.slice();
const modifiers = [];
let elm;
while ((elm = dataList[0]) && typeof elm === "string") {
modifiers.push(dataList.shift());
}
return { modifiers, data: dataList };
}
export const config = {
// whether or not blockdom should normalize DOM whenever a block is created.
// Normalizing dom mean removing empty text nodes (or containing only spaces)
shouldNormalizeDom: true,
// this is the main event handler. Every event handler registered with blockdom
// will go through this function, giving it the data registered in the block
// and the event
mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => {
if (typeof data === "function") {
data(ev);
} else if (Array.isArray(data)) {
data = filterOutModifiersFromData(data).data;
data[0](data[1], ev);
}
return false;
},
};
+90
View File
@@ -0,0 +1,90 @@
import { config } from "./config";
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
interface EventHandlerCreator {
setup: EventHandlerSetter;
update: EventHandlerSetter;
}
export function createEventHandler(rawEvent: string): EventHandlerCreator {
const eventName = rawEvent.split(".")[0];
const capture = rawEvent.includes(".capture");
if (rawEvent.includes(".synthetic")) {
return createSyntheticHandler(eventName, capture);
} else {
return createElementHandler(eventName, capture);
}
}
// Native listener
let nextNativeEventId = 1;
function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator {
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
if (capture) {
eventKey = `${eventKey}_capture`;
}
function listener(ev: Event) {
const currentTarget = ev.currentTarget;
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
const data = (currentTarget as any)[eventKey];
if (!data) return;
config.mainEventHandler(data, ev, currentTarget);
}
function setup(this: HTMLElement, data: any) {
(this as any)[eventKey] = data;
this.addEventListener(evName, listener, { capture });
}
function update(this: HTMLElement, data: any) {
(this as any)[eventKey] = data;
}
return { setup, update };
}
// Synthetic handler: a form of event delegation that allows placing only one
// listener per event type.
let nextSyntheticEventId = 1;
function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator {
let eventKey = `__event__synthetic_${evName}`;
if (capture) {
eventKey = `${eventKey}_capture`;
}
setupSyntheticEvent(evName, eventKey, capture);
const currentId = nextSyntheticEventId++;
function setup(this: HTMLElement, data: any) {
const _data = (this as any)[eventKey] || {};
_data[currentId] = data;
(this as any)[eventKey] = _data;
}
return { setup, update: setup };
}
function nativeToSyntheticEvent(eventKey: string, event: Event) {
let dom = event.target;
while (dom !== null) {
const _data = (dom as any)[eventKey];
if (_data) {
for (const data of Object.values(_data)) {
const stopped = config.mainEventHandler(data, event, dom);
if (stopped) return;
}
}
dom = (dom as any).parentNode;
}
}
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) {
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
return;
}
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {
capture,
});
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
}
+87
View File
@@ -0,0 +1,87 @@
import type { VNode } from "./index";
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeRemoveChild = nodeProto.removeChild;
class VHtml {
html: string;
parentEl?: HTMLElement | undefined;
content: ChildNode[] = [];
constructor(html: string) {
this.html = html;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
const template = document.createElement("template");
template.innerHTML = this.html;
this.content = [...(template.content.childNodes as any)];
for (let elem of this.content) {
nodeInsertBefore.call(parent, elem, afterNode);
}
if (!this.content.length) {
const textNode = document.createTextNode("");
this.content.push(textNode);
nodeInsertBefore.call(parent, textNode, afterNode);
}
}
moveBefore(other: VHtml | null, afterNode: Node | null) {
const target = other ? other.content[0] : afterNode;
const parent = this.parentEl;
for (let elem of this.content) {
nodeInsertBefore.call(parent, elem, target);
}
}
patch(other: VHtml) {
if (this === other) {
return;
}
const html2 = other.html;
if (this.html !== html2) {
const parent = this.parentEl;
// insert new html in front of current
const afterNode = this.content[0];
const template = document.createElement("template");
template.innerHTML = html2;
const content = [...(template.content.childNodes as any)];
for (let elem of content) {
nodeInsertBefore.call(parent, elem, afterNode);
}
if (!content.length) {
const textNode = document.createTextNode("");
content.push(textNode);
nodeInsertBefore.call(parent, textNode, afterNode);
}
// remove current content
this.remove();
this.content = content;
}
}
beforeRemove() {}
remove() {
const parent = this.parentEl;
for (let elem of this.content) {
nodeRemoveChild.call(parent, elem);
}
}
firstNode(): Node {
return this.content[0]!;
}
toString() {
return this.html;
}
}
export function html(str: string): VNode<VHtml> {
return new VHtml(str);
}
+44
View File
@@ -0,0 +1,44 @@
export { config } from "./config";
export { toggler } from "./toggler";
export { createBlock } from "./block_compiler";
export { list } from "./list";
export { multi } from "./multi";
export { text, comment } from "./text";
export { html } from "./html";
export interface VNode<T = any> {
mount(parent: HTMLElement, afterNode: Node | null): void;
moveBefore(other: T | null, afterNode: Node | null): void;
patch(other: T, withBeforeRemove: boolean): void;
beforeRemove(): void;
remove(): void;
firstNode(): Node | undefined;
el?: undefined | HTMLElement | Text;
parentEl?: undefined | HTMLElement;
isOnlyChild?: boolean | undefined;
key?: any;
}
export type BDom = VNode<any>;
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
vnode.mount(fixture, afterNode);
}
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
vnode1.patch(vnode2, withBeforeRemove);
}
export function remove(vnode: VNode, withBeforeRemove: boolean = false) {
if (withBeforeRemove) {
vnode.beforeRemove();
}
vnode.remove();
}
export function withKey(vnode: VNode, key: any) {
vnode.key = key;
return vnode;
}
+241
View File
@@ -0,0 +1,241 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeAppendChild = nodeProto.appendChild;
const nodeRemoveChild = nodeProto.removeChild;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
// -----------------------------------------------------------------------------
// List Node
// -----------------------------------------------------------------------------
class VList {
children: VNode[];
anchor: Node | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: VNode[], deepRemove: boolean) {
this.children = children;
this.deepRemove = deepRemove;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const _anchor = document.createTextNode("");
this.anchor = _anchor;
nodeInsertBefore.call(parent, _anchor, afterNode);
const l = children.length;
if (l) {
const mount = children[0].mount;
for (let i = 0; i < l; i++) {
mount.call(children[i], parent, _anchor);
}
}
this.parentEl = parent;
}
moveBefore(other: VList | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchor) || null;
}
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].moveBefore(null, afterNode);
}
this.parentEl!.insertBefore(this.anchor!, afterNode);
}
patch(other: VList, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const ch1 = this.children;
const ch2: VNode[] = other.children;
if (ch2.length === 0 && ch1.length === 0) {
return;
}
this.children = ch2;
const proto = ch2[0] || ch1[0];
const {
mount: cMount,
patch: cPatch,
remove: cRemove,
beforeRemove,
moveBefore: cMoveBefore,
firstNode: cFirstNode,
} = proto;
const _anchor = this.anchor!;
const isOnlyChild = this.isOnlyChild;
const parent = this.parentEl!;
// fast path: no new child => only remove
if (ch2.length === 0 && isOnlyChild && !this.deepRemove) {
if (withBeforeRemove) {
for (let i = 0, l = ch1.length; i < l; i++) {
beforeRemove.call(ch1[i]);
}
}
nodeSetTextContent.call(parent, "");
nodeAppendChild.call(parent, _anchor);
return;
}
let startIdx1 = 0;
let startIdx2 = 0;
let startVn1 = ch1[0];
let startVn2 = ch2[0];
let endIdx1 = ch1.length - 1;
let endIdx2 = ch2.length - 1;
let endVn1 = ch1[endIdx1];
let endVn2 = ch2[endIdx2];
let mapping: any = undefined;
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
// -------------------------------------------------------------------
if (startVn1 === null) {
startVn1 = ch1[++startIdx1];
continue;
}
// -------------------------------------------------------------------
if (endVn1 === null) {
endVn1 = ch1[--endIdx1];
continue;
}
// -------------------------------------------------------------------
let startKey1 = startVn1.key;
let startKey2 = startVn2.key;
if (startKey1 === startKey2) {
cPatch.call(startVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = startVn1;
startVn1 = ch1[++startIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
let endKey1 = endVn1.key;
let endKey2 = endVn2.key;
if (endKey1 === endKey2) {
cPatch.call(endVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = endVn1;
endVn1 = ch1[--endIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (startKey1 === endKey2) {
// bnode moved right
cPatch.call(startVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = startVn1;
const nextChild = ch2[endIdx2 + 1];
cMoveBefore.call(startVn1, nextChild, _anchor);
startVn1 = ch1[++startIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (endKey1 === startKey2) {
// bnode moved left
cPatch.call(endVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = endVn1;
const nextChild = ch1[startIdx1];
cMoveBefore.call(endVn1, nextChild, _anchor);
endVn1 = ch1[--endIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
mapping = mapping || createMapping(ch1, startIdx1, endIdx1);
let idxInOld = mapping[startKey2];
if (idxInOld === undefined) {
cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);
} else {
const elmToMove = ch1[idxInOld];
cMoveBefore.call(elmToMove, startVn1, null);
cPatch.call(elmToMove, startVn2, withBeforeRemove);
ch2[startIdx2] = elmToMove;
ch1[idxInOld] = null as any;
}
startVn2 = ch2[++startIdx2];
}
// ---------------------------------------------------------------------
if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {
if (startIdx1 > endIdx1) {
const nextChild = ch2[endIdx2 + 1];
const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;
for (let i = startIdx2; i <= endIdx2; i++) {
cMount.call(ch2[i], parent, anchor);
}
} else {
for (let i = startIdx1; i <= endIdx1; i++) {
let ch = ch1[i];
if (ch) {
if (withBeforeRemove) {
beforeRemove.call(ch);
}
cRemove.call(ch);
}
}
}
}
}
beforeRemove() {
const children = this.children;
const l = children.length;
if (l) {
const beforeRemove = children[0].beforeRemove;
for (let i = 0; i < l; i++) {
beforeRemove.call(children[i]);
}
}
}
remove() {
const { parentEl, anchor } = this;
if (this.isOnlyChild && !this.deepRemove) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const l = children.length;
if (l) {
const remove = children[0].remove;
for (let i = 0; i < l; i++) {
remove.call(children[i]);
}
}
nodeRemoveChild.call(parentEl, anchor!);
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : undefined;
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
}
}
export function list(children: VNode[], deepRemove = false): VNode<VList> {
return new VList(children, deepRemove);
}
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
let mapping: any = {};
for (let i = startIdx1; i <= endIdx2; i++) {
mapping[ch1[i].key] = i;
}
return mapping;
}
+136
View File
@@ -0,0 +1,136 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
const nodeRemoveChild = nodeProto.removeChild;
// -----------------------------------------------------------------------------
// Multi NODE
// -----------------------------------------------------------------------------
export class VMulti {
children: (VNode | undefined)[];
anchors?: Node[] | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: (VNode | undefined)[], deepRemove: boolean) {
this.children = children;
this.deepRemove = deepRemove;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const l = children.length;
const anchors = new Array(l);
for (let i = 0; i < l; i++) {
let child = children[i];
if (child) {
child.mount(parent, afterNode);
} else {
const childAnchor = document.createTextNode("");
anchors[i] = childAnchor;
nodeInsertBefore.call(parent, childAnchor, afterNode);
}
}
this.anchors = anchors;
this.parentEl = parent;
}
moveBefore(other: VMulti | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
}
const children = this.children;
const parent = this.parentEl;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
if (child) {
child.moveBefore(null, afterNode);
} else {
const anchor = anchors![i];
nodeInsertBefore.call(parent, anchor, afterNode);
}
}
}
patch(other: VMulti, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const children1 = this.children;
const children2 = other.children;
const anchors = this.anchors!;
const parentEl = this.parentEl!;
for (let i = 0, l = children1.length; i < l; i++) {
const vn1 = children1[i];
const vn2 = children2[i];
if (vn1) {
if (vn2) {
vn1.patch(vn2, withBeforeRemove);
} else {
const afterNode = vn1.firstNode()!;
const anchor = document.createTextNode("");
anchors[i] = anchor;
nodeInsertBefore.call(parentEl, anchor, afterNode);
if (withBeforeRemove) {
vn1.beforeRemove();
}
vn1.remove();
children1[i] = undefined;
}
} else if (vn2) {
children1[i] = vn2;
const anchor = anchors[i];
vn2.mount(parentEl, anchor);
nodeRemoveChild.call(parentEl, anchor);
}
}
}
beforeRemove() {
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.beforeRemove();
}
}
}
remove() {
const parentEl = this.parentEl;
if (this.isOnlyChild && !this.deepRemove) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.remove();
} else {
nodeRemoveChild.call(parentEl, anchors![i]);
}
}
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : this.anchors![0];
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
}
}
export function multi(children: (VNode | undefined)[], deepRemove = false): VNode<VMulti> {
return new VMulti(children, deepRemove);
}
+87
View File
@@ -0,0 +1,87 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const characterDataProto = CharacterData.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeRemoveChild = nodeProto.removeChild;
abstract class VSimpleNode {
text: string;
parentEl?: HTMLElement | undefined;
el?: any;
constructor(text: string) {
this.text = text;
}
mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
nodeInsertBefore.call(parent, node, afterNode);
this.el = node;
}
moveBefore(other: VText | null, afterNode: Node | null) {
const target = other ? other.el! : afterNode;
nodeInsertBefore.call(this.parentEl, this.el!, target);
}
beforeRemove() {}
remove() {
nodeRemoveChild.call(this.parentEl, this.el!);
}
firstNode(): Node {
return this.el!;
}
toString() {
return this.text;
}
}
class VText extends VSimpleNode {
mount(parent: HTMLElement, afterNode: Node | null) {
this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);
}
patch(other: VText) {
const text2 = other.text;
if (this.text !== text2) {
characterDataSetData.call(this.el!, toText(text2));
this.text = text2;
}
}
}
class VComment extends VSimpleNode {
mount(parent: HTMLElement, afterNode: Node | null) {
this.mountNode(document.createComment(toText(this.text)), parent, afterNode);
}
patch() {}
}
export function text(str: string): VNode<VText> {
return new VText(str);
}
export function comment(str: string): VNode<VComment> {
return new VComment(str);
}
export function toText(value: any): string {
switch (typeof value) {
case "string":
return value;
case "number":
return String(value);
case "boolean":
return value ? "true" : "false";
default:
return value || "";
}
}
+65
View File
@@ -0,0 +1,65 @@
import type { VNode } from "./index";
// -----------------------------------------------------------------------------
// Toggler node
// -----------------------------------------------------------------------------
class VToggler {
key: string;
child: VNode;
parentEl?: HTMLElement | undefined;
constructor(key: string, child: VNode) {
this.key = key;
this.child = child;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
this.child.mount(parent, afterNode);
}
moveBefore(other: VToggler | null, afterNode: Node | null) {
this.child.moveBefore(other ? other.child : null, afterNode);
}
patch(other: VToggler, withBeforeRemove: boolean) {
if (this === other) {
return;
}
let child1 = this.child;
let child2 = other.child;
if (this.key === other.key) {
child1.patch(child2, withBeforeRemove);
} else {
child2.mount(this.parentEl!, child1.firstNode()!);
if (withBeforeRemove) {
child1.beforeRemove();
}
child1.remove();
this.child = child2;
this.key = other.key;
}
}
beforeRemove() {
this.child.beforeRemove();
}
remove() {
this.child.remove();
}
firstNode(): Node | undefined {
return this.child.firstNode();
}
toString(): string {
return this.child.toString();
}
}
export function toggler(key: string, child: VNode): VNode<VToggler> {
return new VToggler(key, child);
}
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
import type { BDom } from "../blockdom";
import { CodeGenerator, Config } from "./code_generator";
import { parse } from "./parser";
export type Template = (context: any, vnode: any, key?: string) => BDom;
export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config {
name?: string;
}
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
// parsing
const ast = parse(template);
// some work
const hasSafeContext =
template instanceof Node
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
: !template.includes("t-set") && !template.includes("t-call");
// code generation
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
// template function
return new Function("bdom, helpers", code) as TemplateFunction;
}
@@ -25,30 +25,27 @@
// Misc types, constants and helpers // 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 RESERVED_WORDS =
"," "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
); ","
);
const WORD_REPLACEMENT = { const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
and: "&&", and: "&&",
or: "||", or: "||",
gt: ">", gt: ">",
gte: ">=", gte: ">=",
lt: "<", lt: "<",
lte: "<=" lte: "<=",
}; });
export interface QWebExprVar { export interface QWebVar {
id: string; id: string; // foo
expr: string; expr: string; // scope.foo (local variables => only foo)
value?: string; // 1 + 3
hasBody?: boolean;
} }
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tokenizer // Tokenizer
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -61,6 +58,7 @@ type TKind =
| "RIGHT_PAREN" | "RIGHT_PAREN"
| "COMMA" | "COMMA"
| "VALUE" | "VALUE"
| "TEMPLATE_STRING"
| "SYMBOL" | "SYMBOL"
| "OPERATOR" | "OPERATOR"
| "COLON"; | "COLON";
@@ -68,10 +66,14 @@ type TKind =
interface Token { interface Token {
type: TKind; type: TKind;
value: string; value: string;
originalValue?: string;
size?: number; size?: number;
varName?: string;
replace?: Function;
isLocal?: boolean;
} }
const STATIC_TOKEN_MAP: { [key: string]: TKind } = { const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
"{": "LEFT_BRACE", "{": "LEFT_BRACE",
"}": "RIGHT_BRACE", "}": "RIGHT_BRACE",
"[": "LEFT_BRACKET", "[": "LEFT_BRACKET",
@@ -79,17 +81,19 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
":": "COLON", ":": "COLON",
",": "COMMA", ",": "COMMA",
"(": "LEFT_PAREN", "(": "LEFT_PAREN",
")": "RIGHT_PAREN" ")": "RIGHT_PAREN",
}; });
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(","); // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) { let tokenizeString: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
let start = s; let start = s;
if (s !== "'" && s !== '"') { if (s !== "'" && s !== '"' && s !== "`") {
return false; return false;
} }
let i = 1; let i = 1;
@@ -111,10 +115,21 @@ let tokenizeString: Tokenizer = function(expr) {
throw new Error("Invalid expression"); throw new Error("Invalid expression");
} }
s += start; s += start;
if (start === "`") {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer: any) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
},
};
}
return { type: "VALUE", value: s }; return { type: "VALUE", value: s };
}; };
let tokenizeNumber: Tokenizer = function(expr) { let tokenizeNumber: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[0-9]/)) { if (s && s.match(/[0-9]/)) {
let i = 1; let i = 1;
@@ -128,7 +143,7 @@ let tokenizeNumber: Tokenizer = function(expr) {
} }
}; };
let tokenizeSymbol: Tokenizer = function(expr) { let tokenizeSymbol: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) { if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1; let i = 1;
@@ -145,7 +160,7 @@ let tokenizeSymbol: Tokenizer = function(expr) {
} }
}; };
const tokenizeStatic: Tokenizer = function(expr) { const tokenizeStatic: Tokenizer = function (expr) {
const char = expr[0]; const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) { if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char }; return { type: STATIC_TOKEN_MAP[char], value: char };
@@ -153,7 +168,7 @@ const tokenizeStatic: Tokenizer = function(expr) {
return false; return false;
}; };
const tokenizeOperator: Tokenizer = function(expr) { const tokenizeOperator: Tokenizer = function (expr) {
for (let op of OPERATORS) { for (let op of OPERATORS) {
if (expr.startsWith(op)) { if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op }; return { type: "OPERATOR", value: op };
@@ -165,9 +180,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
const TOKENIZERS = [ const TOKENIZERS = [
tokenizeString, tokenizeString,
tokenizeNumber, tokenizeNumber,
tokenizeOperator,
tokenizeSymbol, tokenizeSymbol,
tokenizeStatic, tokenizeStatic,
tokenizeOperator
]; ];
/** /**
@@ -210,6 +225,11 @@ export function tokenize(expr: string): Token[] {
// Expression "evaluator" // Expression "evaluator"
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const isLeftSeparator = (token: Token) =>
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token: Token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/** /**
* This is the main function exported by this file. This is the code that will * 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 * process an expression (given as a string) and returns another expression with
@@ -230,35 +250,107 @@ export function tokenize(expr: string): Token[] {
* - unless the previous token is a dot (in that case, this is a property: `a.b`) * - 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 * - 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}`) * a colon (in that case, this is an object key: `{a: b}`)
*
* Some specific code is also required to support arrow functions. If we detect
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/ */
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string { export function compileExprToArray(expr: string): Token[] {
const localVars = new Set<string>();
const tokens = tokenize(expr); const tokens = tokenize(expr);
let result = ""; let i = 0;
for (let i = 0; i < tokens.length; i++) { let stack = []; // to track last opening [ or {
while (i < tokens.length) {
let token = tokens[i]; let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { if (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) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
if (prevToken.type === "OPERATOR" && prevToken.value === ".") { if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false; isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { } else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") { if (nextToken && nextToken.type === "COLON") {
isVar = false; isVar = false;
} }
} }
} }
if (isVar) { }
if (token.value in vars && "id" in vars[token.value]) { if (token.type === "TEMPLATE_STRING") {
token.value = (<QWebExprVar>vars[token.value]).id; token.value = token.replace!((expr: any) => compileExpr(expr));
} else { }
token.value = `context['${token.value}']`; if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
} }
} else {
localVars.add(token.value); //] = { id: token.value, expr: token.value };
} }
} }
result += token.value;
if (isVar) {
token.varName = token.value;
if (!localVars.has(token.value)) {
token.originalValue = token.value;
token.value = `ctx['${token.value}']`;
}
}
i++;
} }
return result; // Mark all variables that have been used locally.
// This assumes the expression has only one scope (incorrect but "good enough for now")
for (const token of tokens) {
if (token.type === "SYMBOL" && localVars.has(token.value)) {
token.isLocal = true;
}
}
return tokens;
}
export function compileExpr(expr: string): string {
return compileExprToArray(expr)
.map((t) => t.value)
.join("");
}
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
export function interpolate(s: string): string {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${compileExpr(s.slice(2, -2))})`;
}
let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
return "`" + r + "`";
} }
+997
View File
@@ -0,0 +1,997 @@
// -----------------------------------------------------------------------------
// AST Type definition
// -----------------------------------------------------------------------------
export const enum ASTType {
Text,
Comment,
DomNode,
Multi,
TEsc,
TIf,
TSet,
TCall,
TOut,
TForEach,
TKey,
TComponent,
TDebug,
TLog,
TSlot,
TCallBlock,
TTranslation,
TPortal,
}
export interface ASTText {
type: ASTType.Text;
value: string;
}
export interface ASTComment {
type: ASTType.Comment;
value: string;
}
export interface ASTDomNode {
type: ASTType.DomNode;
tag: string;
dynamicTag: string | null;
attrs: { [key: string]: string };
content: AST[];
ref: string | null;
on: { [key: string]: string };
model: {
baseExpr: string;
expr: string;
targetAttr: string;
specialInitTargetAttr: string | null;
eventType: "change" | "click" | "input";
shouldTrim: boolean;
shouldNumberize: boolean;
} | null;
ns: string | null;
}
export interface ASTMulti {
type: ASTType.Multi;
content: AST[];
deepRemove: boolean;
}
export interface ASTTEsc {
type: ASTType.TEsc;
expr: string;
defaultValue: string;
}
export interface ASTTOut {
type: ASTType.TOut;
expr: string;
body: AST[] | null;
}
export interface ASTTif {
type: ASTType.TIf;
condition: string;
content: AST;
tElif: { condition: string; content: AST }[] | null;
tElse: AST | null;
deepRemove: boolean;
}
export interface ASTTSet {
type: ASTType.TSet;
name: string;
value: string | null; // value defined in attribute
defaultValue: string | null; // value defined in body, if text
body: AST[] | null; // content of body if not text
}
export interface ASTTForEach {
type: ASTType.TForEach;
collection: string;
elem: string;
key: string | null;
body: AST;
memo: string;
deepRemove: boolean;
hasNoFirst: boolean;
hasNoLast: boolean;
hasNoIndex: boolean;
hasNoValue: boolean;
}
export interface ASTTKey {
type: ASTType.TKey;
expr: string;
content: AST;
}
export interface ASTTCall {
type: ASTType.TCall;
name: string;
body: AST[] | null;
}
export interface ASTComponent {
type: ASTType.TComponent;
name: string;
isDynamic: boolean;
dynamicProps: string | null;
props: { [name: string]: string };
slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } };
}
export interface ASTSlot {
type: ASTType.TSlot;
name: string;
attrs: { [key: string]: string };
defaultContent: AST | null;
}
export interface ASTTCallBlock {
type: ASTType.TCallBlock;
name: string;
}
export interface ASTDebug {
type: ASTType.TDebug;
content: AST | null;
}
export interface ASTLog {
type: ASTType.TLog;
expr: string;
content: AST | null;
}
export interface ASTTranslation {
type: ASTType.TTranslation;
content: AST | null;
}
export interface ASTTPortal {
type: ASTType.TPortal;
target: string;
content: AST;
}
export type AST =
| ASTText
| ASTComment
| ASTDomNode
| ASTMulti
| ASTTEsc
| ASTTif
| ASTTSet
| ASTTCall
| ASTTOut
| ASTTForEach
| ASTTKey
| ASTComponent
| ASTSlot
| ASTTCallBlock
| ASTLog
| ASTDebug
| ASTTranslation
| ASTTPortal;
// -----------------------------------------------------------------------------
// Parser
// -----------------------------------------------------------------------------
interface ParsingContext {
inPreTag: boolean;
inSVG: boolean;
}
export function parse(xml: string | Node): AST {
const node = xml instanceof Element ? xml : (parseXML(`<t>${xml}</t>`).firstChild! as Element);
normalizeXML(node);
const ctx = { inPreTag: false, inSVG: false };
const ast = parseNode(node, ctx);
if (!ast) {
return { type: ASTType.Text, value: "" };
}
return ast;
}
function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
if (!(node instanceof Element)) {
return parseTextCommentNode(node, ctx);
}
return (
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
parseTCall(node, ctx) ||
parseTCallBlock(node, ctx) ||
parseTEscNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) ||
parseTOutNode(node, ctx) ||
parseComponent(node, ctx) ||
parseDOMNode(node, ctx) ||
parseTSetNode(node, ctx) ||
parseTNode(node, ctx)
);
}
// -----------------------------------------------------------------------------
// <t /> tag
// -----------------------------------------------------------------------------
function parseTNode(node: Element, ctx: ParsingContext): AST | null {
if (node.tagName !== "t") {
return null;
}
return parseChildNodes(node, ctx);
}
// -----------------------------------------------------------------------------
// Text and Comment Nodes
// -----------------------------------------------------------------------------
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) {
return null;
}
value = value.replace(whitespaceRE, " ");
}
return { type: ASTType.Text, value };
} else if (node.nodeType === Node.COMMENT_NODE) {
return { type: ASTType.Comment, value: node.textContent || "" };
}
return null;
}
// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
if (node.hasAttribute("t-debug")) {
node.removeAttribute("t-debug");
return {
type: ASTType.TDebug,
content: parseNode(node, ctx),
};
}
if (node.hasAttribute("t-log")) {
const expr = node.getAttribute("t-log")!;
node.removeAttribute("t-log");
return {
type: ASTType.TLog,
expr,
content: parseNode(node, ctx),
};
}
return null;
}
// -----------------------------------------------------------------------------
// Regular dom node
// -----------------------------------------------------------------------------
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node;
const dynamicTag = node.getAttribute("t-tag");
node.removeAttribute("t-tag");
if (tagName === "t" && !dynamicTag) {
return null;
}
ctx = Object.assign({}, ctx);
if (tagName === "pre") {
ctx.inPreTag = true;
}
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const children = parseChildren(node, ctx);
const nodeAttrsNames = node.getAttributeNames();
const attrs: ASTDomNode["attrs"] = {};
const on: ASTDomNode["on"] = {};
let model: ASTDomNode["model"] = null;
for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr)!;
if (attr.startsWith("t-on")) {
if (attr === "t-on") {
throw new Error("Missing event name with t-on directive");
}
on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) {
if (!["input", "select", "textarea"].includes(tagName)) {
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
}
let baseExpr, expr;
if (hasDotAtTheEnd.test(value)) {
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
expr = `'${value.slice(index + 1)}'`;
} else if (hasBracketsAtTheEnd.test(value)) {
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
expr = value.slice(index + 1, -1);
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
const typeAttr = node.getAttribute("type");
const isInput = tagName === "input";
const isSelect = tagName === "select";
const isTextarea = tagName === "textarea";
const isCheckboxInput = isInput && typeAttr === "checkbox";
const isRadioInput = isInput && typeAttr === "radio";
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
const hasLazyMod = attr.includes(".lazy");
const hasNumberMod = attr.includes(".number");
const hasTrimMod = attr.includes(".trim");
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
model = {
baseExpr,
expr,
targetAttr: isCheckboxInput ? "checked" : "value",
specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType,
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
};
} else {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`);
}
attrs[attr] = value;
}
}
return {
type: ASTType.DomNode,
tag: tagName,
dynamicTag,
attrs,
on,
ref,
content: children,
model,
ns,
};
}
// -----------------------------------------------------------------------------
// t-esc
// -----------------------------------------------------------------------------
function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-esc")) {
return null;
}
const escValue = node.getAttribute("t-esc")!;
node.removeAttribute("t-esc");
const tesc: AST = {
type: ASTType.TEsc,
expr: escValue,
defaultValue: node.textContent || "",
};
let ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const ast = parseNode(node, ctx);
if (!ast) {
return tesc;
}
if (ast.type === ASTType.DomNode) {
return {
...ast,
ref,
content: [tesc],
};
}
if (ast.type === ASTType.TComponent) {
throw new Error("t-esc is not supported on Component nodes");
}
return tesc;
}
// -----------------------------------------------------------------------------
// t-out
// -----------------------------------------------------------------------------
function parseTOutNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
return null;
}
if (node.hasAttribute("t-raw")) {
console.warn(
`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`
);
}
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"))!;
node.removeAttribute("t-out");
node.removeAttribute("t-raw");
const tOut: AST = { type: ASTType.TOut, expr, body: null };
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const ast = parseNode(node, ctx);
if (!ast) {
return tOut;
}
if (ast.type === ASTType.DomNode) {
tOut.body = ast.content.length ? ast.content : null;
return {
...ast,
ref,
content: [tOut],
};
}
return tOut;
}
// -----------------------------------------------------------------------------
// t-foreach and t-key
// -----------------------------------------------------------------------------
function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-foreach")) {
return null;
}
const html = node.outerHTML;
const collection = node.getAttribute("t-foreach")!;
node.removeAttribute("t-foreach");
const elem = node.getAttribute("t-as") || "";
node.removeAttribute("t-as");
const key = node.getAttribute("t-key");
if (!key) {
throw new Error(
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
);
}
node.removeAttribute("t-key");
const memo = node.getAttribute("t-memo") || "";
node.removeAttribute("t-memo");
const body = parseNode(node, ctx);
if (!body) {
return null;
}
const hasNoTCall = !html.includes("t-call");
const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
return {
type: ASTType.TForEach,
collection,
elem,
body,
memo,
deepRemove: needDeepRemove(body),
key,
hasNoFirst,
hasNoLast,
hasNoIndex,
hasNoValue,
};
}
/**
* @returns true if we are sure that a deep remove (without optimisation) is needed, for exemple
* if there is a portal.
*/
function needDeepRemove(ast: AST): boolean {
switch (ast.type) {
case ASTType.Multi:
case ASTType.TForEach:
case ASTType.TIf:
return ast.deepRemove;
case ASTType.TPortal:
return true;
case ASTType.TComponent:
case ASTType.TOut:
case ASTType.TCall:
case ASTType.TCallBlock:
case ASTType.TSlot:
case ASTType.Text:
case ASTType.Comment:
case ASTType.TEsc:
return false;
case ASTType.TKey:
return needDeepRemove(ast.content);
case ASTType.TDebug:
case ASTType.TLog:
case ASTType.TTranslation:
return ast.content ? needDeepRemove(ast.content) : false;
case ASTType.TSet:
return ast.body ? ast.body.some((ast) => needDeepRemove(ast)) : false;
case ASTType.DomNode:
return ast.content.some((ast) => needDeepRemove(ast));
}
}
function parseTKey(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-key")) {
return null;
}
const key = node.getAttribute("t-key")!;
node.removeAttribute("t-key");
const body = parseNode(node, ctx);
if (!body) {
return null;
}
return { type: ASTType.TKey, expr: key, content: body };
}
// -----------------------------------------------------------------------------
// t-call
// -----------------------------------------------------------------------------
function parseTCall(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-call")) {
return null;
}
const subTemplate = node.getAttribute("t-call")!;
node.removeAttribute("t-call");
if (node.tagName !== "t") {
const ast = parseNode(node, ctx);
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null };
if (ast && ast.type === ASTType.DomNode) {
ast.content = [tcall];
return ast;
}
if (ast && ast.type === ASTType.TComponent) {
return {
...ast,
slots: { default: { content: tcall } },
};
}
}
const body = parseChildren(node, ctx);
return {
type: ASTType.TCall,
name: subTemplate,
body: body.length ? body : null,
};
}
// -----------------------------------------------------------------------------
// t-call-block
// -----------------------------------------------------------------------------
function parseTCallBlock(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-call-block")) {
return null;
}
const name = node.getAttribute("t-call-block")!;
return {
type: ASTType.TCallBlock,
name,
};
}
// -----------------------------------------------------------------------------
// t-if
// -----------------------------------------------------------------------------
function parseTIf(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-if")) {
return null;
}
const condition = node.getAttribute("t-if")!;
node.removeAttribute("t-if");
const content = parseNode(node, ctx) || { type: ASTType.Text, value: "" };
let nextElement = node.nextElementSibling;
// t-elifs
const tElifs: any[] = [];
while (nextElement && nextElement.hasAttribute("t-elif")) {
const condition = nextElement.getAttribute("t-elif");
nextElement.removeAttribute("t-elif");
const tElif = parseNode(nextElement, ctx);
const next = nextElement.nextElementSibling;
nextElement.remove();
nextElement = next;
if (tElif) {
tElifs.push({ condition, content: tElif });
}
}
// t-else
let tElse: AST | null = null;
if (nextElement && nextElement.hasAttribute("t-else")) {
nextElement.removeAttribute("t-else");
tElse = parseNode(nextElement, ctx);
nextElement.remove();
}
let deepRemove = needDeepRemove(content);
if (tElifs) {
deepRemove = deepRemove || tElifs.some((ast) => needDeepRemove(ast));
}
if (tElse) {
deepRemove = deepRemove || needDeepRemove(tElse);
}
return {
type: ASTType.TIf,
condition,
content,
tElif: tElifs.length ? tElifs : null,
tElse,
deepRemove,
};
}
// -----------------------------------------------------------------------------
// t-set directive
// -----------------------------------------------------------------------------
function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-set")) {
return null;
}
const name = node.getAttribute("t-set")!;
const value = node.getAttribute("t-value") || null;
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
let body: AST[] | null = null;
if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx);
}
return { type: ASTType.TSet, name, value, defaultValue, body };
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
// Error messages when trying to use an unsupported directive on a component
const directiveErrorMap = new Map([
["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
[
"t-ref",
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
],
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
[
"t-attf",
"t-attf is not supported on components: use template strings for string interpolation in props",
],
]);
function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let name = node.tagName;
const firstLetter = name[0];
let isDynamic = node.hasAttribute("t-component");
if (isDynamic && name !== "t") {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
}
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
return null;
}
if (isDynamic) {
name = node.getAttribute("t-component")!;
node.removeAttribute("t-component");
}
const dynamicProps = node.getAttribute("t-props");
node.removeAttribute("t-props");
const props: ASTComponent["props"] = {};
for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!;
if (name.startsWith("t-")) {
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
throw new Error(message || `unsupported directive on Component: ${name}`);
} else {
props[name] = value;
}
}
const slots: ASTComponent["slots"] = {};
if (node.hasChildNodes()) {
const clone = <Element>node.cloneNode(true);
// named slots
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
for (let slotNode of slotNodes) {
if (slotNode.tagName !== "t") {
throw new Error(
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
);
}
const name = slotNode.getAttribute("t-set-slot")!;
// check if this is defined in a sub component (in which case it should
// be ignored)
let el = slotNode.parentElement!;
let isInSubComponent = false;
while (el !== clone) {
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
isInSubComponent = true;
break;
}
el = el.parentElement!;
}
if (isInSubComponent) {
continue;
}
slotNode.removeAttribute("t-set-slot");
slotNode.remove();
const slotAst = parseNode(slotNode, ctx);
if (slotAst) {
const slotInfo: any = { content: slotAst };
const attrs: { [key: string]: string } = {};
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
slotInfo.scope = value;
continue;
}
attrs[attributeName] = value;
}
if (Object.keys(attrs).length) {
slotInfo.attrs = attrs;
}
slots[name] = slotInfo;
}
}
// default slot
const defaultContent = parseChildNodes(clone, ctx);
if (defaultContent) {
slots.default = { content: defaultContent };
}
}
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
}
// -----------------------------------------------------------------------------
// Slots
// -----------------------------------------------------------------------------
function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-slot")) {
return null;
}
const name = node.getAttribute("t-slot")!;
node.removeAttribute("t-slot");
const attrs: { [key: string]: string } = {};
for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName)!;
attrs[attributeName] = value;
}
return {
type: ASTType.TSlot,
name,
attrs,
defaultContent: parseChildNodes(node, ctx),
};
}
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") {
return null;
}
node.removeAttribute("t-translation");
return {
type: ASTType.TTranslation,
content: parseNode(node, ctx),
};
}
// -----------------------------------------------------------------------------
// Portal
// -----------------------------------------------------------------------------
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-portal")) {
return null;
}
if (node.tagName !== "t") {
throw new Error(
`Directive 't-portal' can only be used on <t> nodes (used on a <${node.tagName}>)`
);
}
const target = node.getAttribute("t-portal")!;
node.removeAttribute("t-portal");
const content = parseNode(node, ctx);
if (!content) {
return {
type: ASTType.Text,
value: "",
};
}
return {
type: ASTType.TPortal,
target,
content,
};
}
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
/**
* Parse all the child nodes of a given node and return a list of ast elements
*/
function parseChildren(node: Node, ctx: ParsingContext): AST[] {
const children: AST[] = [];
for (let child of node.childNodes) {
const childAst = parseNode(child, ctx);
if (childAst) {
if (childAst.type === ASTType.Multi) {
children.push(...childAst.content);
} else {
children.push(childAst);
}
}
}
return children;
}
/**
* Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti.
*/
function parseChildNodes(node: Node, ctx: ParsingContext): AST | null {
const children = parseChildren(node, ctx);
switch (children.length) {
case 0:
return null;
case 1:
return children[0];
default:
return {
type: ASTType.Multi,
content: children,
deepRemove: children.some((ast) => needDeepRemove(ast)),
};
}
}
/**
* Normalizes the content of an Element so that t-if/t-elif/t-else directives
* immediately follow one another (by removing empty text nodes or comments).
* Throws an error when a conditional branching statement is malformed. This
* function modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeTIf(el: Element) {
let tbranch = el.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 = (name: string) => prevElem.getAttribute(name);
let nattr = (name: string) => +!!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 (with only spaces) and comment nodes (nodeType 8) between
// branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
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"
);
}
}
}
/**
* Normalizes the content of an Element so that t-esc directives on components
* are removed and instead places a <t t-esc=""> as the default slot of the
* component. Also throws if the component already has content. This function
* modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeTEsc(el: Element) {
const elements = [...el.querySelectorAll("[t-esc]")].filter(
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
);
for (const el of elements) {
if (el.childNodes.length) {
throw new Error("Cannot have t-esc on a component that already has content");
}
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute("t-esc", value);
}
el.appendChild(t);
}
}
/**
* Normalizes the tree inside a given element and do some preliminary validation
* on it. This function modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeXML(el: Element) {
normalizeTIf(el);
normalizeTEsc(el);
}
/**
* Parses an XML string into an XML document, throwing errors on parser errors
* instead of returning an XML document containing the parseerror.
*
* @param xml the string to parse
* @returns an XML document corresponding to the content of the string
*/
function parseXML(xml: string): XMLDocument {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc;
}
+17 -715
View File
@@ -1,725 +1,27 @@
import { Observer } from "../core/observer"; import type { Env } from "../app/app";
import { CompiledTemplate, QWeb } from "../qweb/index"; import type { ComponentNode } from "./component_node";
import { h, patch, VNode } from "../vdom/index";
import "./directive";
import "./props_validation";
/** // -----------------------------------------------------------------------------
* Owl Component System // Component Class
* // -----------------------------------------------------------------------------
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component)
* - the Component class
*/
//------------------------------------------------------------------------------ export class Component {
// Types/helpers static template: string = "";
//------------------------------------------------------------------------------
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env {
qweb: QWeb;
[key: string]: any;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
vnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
renderId: number;
// the renderProps and renderPromise keys are only useful for the "prepare"
// step of the lifecycle of a component. Once a component has been rendered
// and patched, it is no longer useful.
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function };
classObj: { [key: string]: boolean } | null;
}
// If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by
// qweb instances.
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> {
readonly __owl__: Internal<Env, Props>;
template?: string;
/**
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
env: T;
state?: State;
props: Props;
// type of props is not easily representable in typescript...
static props?: any; static props?: any;
static defaultProps?: any;
refs: { props: any;
[key: string]: Component<T, any, any> | HTMLElement | undefined; env: Env;
} = {}; __owl__: ComponentNode;
//-------------------------------------------------------------------------- constructor(props: any, env: Env, node: ComponentNode) {
// Lifecycle this.props = props;
//-------------------------------------------------------------------------- this.env = env;
this.__owl__ = node;
/**
* Creates an instance of Component.
*
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
} else {
this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
}
this.__owl__ = {
id: id,
vnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {},
mountedHandlers: {},
observer: null,
render: null,
classObj: null
};
} }
/** setup() {}
* 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() {}
/** render() {
* mounted is a hook that is called each time a component is attached to the this.__owl__.render();
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return;
}
if (!__owl__.vnode) {
const vnode = await this.__prepare();
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
const patchQueue = [];
await this.__render(false, patchQueue, undefined, undefined);
this.__applyPatchQueue(<any[]>patchQueue);
}
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.isMounted) {
this.__callWillUnmount();
this.el!.remove();
}
}
/**
* The render method is the main entry point to render a component (once it
* is ready. This method is not initially called when the component is
* rendered the first time).
*
* This method will cause all its sub components to potentially rerender
* themselves. Note that `render` is not called if a component is updated via
* its props.
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
return;
}
const patchQueue = [];
const renderId = ++__owl__.renderId;
await this.__render(force, patchQueue, undefined, undefined);
if (__owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
this.__applyPatchQueue(<any[]>patchQueue);
}
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
* static config keys, such as a boolean to determine if we are in mobile
* mode or not.
*/
async updateEnv(nextEnv: Partial<T>): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (__owl__.isMounted) {
await this.render(true);
}
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
* level destroyed component, for performance reasons).
*
* The job of this method is mostly to call willUnmount hooks, and to perform
* all necessary internal cleanup.
*
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp.__callMounted();
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
}
__callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp.__callWillUnmount();
}
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[],
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.__render(forceUpdate, patchQueue, scope, vars);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise;
}
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
const qweb = this.env.qweb;
if (!this.template) {
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
}
let p = (<any>this).constructor;
let name: string = p.name;
let template = tmap[name];
if (template) {
this.template = template;
} else {
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
tmap[name] = template;
this.template = template;
}
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState();
return this.__render(false, [], scope, vars);
}
__render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__;
const promises: Promise<void>[] = [];
const patch: any[] = [this];
patchQueue.push(patch);
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode);
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child component can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
// we applly here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode);
}
/**
* Only called by qweb t-component directive
*/
__mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
*/
__applyPatchQueue(patchQueue: any[]) {
let component = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
component = patch[0];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
component = patch[0];
patch[0].patched(patch[2]);
}
} catch (e) {
errorHandler(e, component);
}
}
}
//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, component: Component<any, any, any>) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent!;
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
} }
} }
+310
View File
@@ -0,0 +1,310 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { Component } from "./component";
import {
Fiber,
makeChildFiber,
makeRootFiber,
MountFiber,
MountOptions,
RootFiber,
} from "./fibers";
import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
export function useComponent(): Component {
return currentNode!.component;
}
export function component(
name: string | typeof Component,
props: any,
key: string,
ctx: ComponentNode,
parent: any
): ComponentNode {
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node) {
if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
}
const parentFiber = ctx.fiber!;
if (node) {
node.updateAndRender(props, parentFiber);
} else {
// new component
let C;
if (isDynamic) {
C = name;
} else {
C = parent.constructor.components[name as any];
if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`);
}
}
node = new ComponentNode(C, props, ctx.app, ctx);
ctx.children[key] = node;
const fiber = makeChildFiber(node, parentFiber);
node.initiateRender(fiber);
}
return node;
}
// -----------------------------------------------------------------------------
// Component VNode
// -----------------------------------------------------------------------------
type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component>
implements VNode<ComponentNode>
{
el?: HTMLElement | Text | undefined;
app: App;
fiber: Fiber | null = null;
component: InstanceType<T>;
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
renderFn: Function;
parent: ComponentNode | null;
level: number;
childEnv: Env;
children: { [key: string]: ComponentNode } = Object.create(null);
refs: any = {};
willStart: LifecycleHook[] = [];
willUpdateProps: LifecycleHook[] = [];
willUnmount: LifecycleHook[] = [];
mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
constructor(C: T, props: any, app: App, parent?: ComponentNode) {
currentNode = this;
this.app = app;
this.parent = parent || null;
this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
}
mountComponent(target: any, options?: MountOptions) {
const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber);
}
async initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) {
fiber.root.mounted.push(fiber);
}
const component = this.component;
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
this._render(fiber);
}
}
async render() {
let current = this.fiber;
if (current && current.root.locked) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
}
if (current && !current.bdom && !fibersInError.has(current)) {
return;
}
if (!this.bdom && !current) {
return;
}
const fiber = makeRootFiber(this);
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
return;
}
// We only want to actually render the component if the following two
// conditions are true:
// * this.fiber: it could be null, in which case the render has been cancelled
// * (current || !fiber.parent): if current is not null, this means that the
// render function was called when a render was already occurring. In this
// case, the pending rendering was cancelled, and the fiber needs to be
// rendered to complete the work. If current is null, we check that the
// fiber has no parent. If that is the case, the fiber was downgraded from
// a root fiber to a child fiber in the previous microtick, because it was
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber && (current || !fiber.parent)) {
this._render(fiber);
}
}
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
fiber.root.counter--;
} catch (e) {
handleError({ node: this, error: e });
}
}
destroy() {
let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy();
if (shouldRemove) {
this.bdom!.remove();
}
}
_destroy() {
const component = this.component;
if (this.status === STATUS.MOUNTED) {
for (let cb of this.willUnmount) {
cb.call(component);
}
}
for (let child of Object.values(this.children)) {
child._destroy();
}
for (let cb of this.willDestroy) {
cb.call(component);
}
this.status = STATUS.DESTROYED;
}
async updateAndRender(props: any, parentFiber: Fiber) {
// update
const fiber = makeChildFiber(this, parentFiber);
this.fiber = fiber;
const component = this.component;
applyDefaultProps(props, component.constructor as any);
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
component.props = props;
this._render(fiber);
const parentRoot = parentFiber.root;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
}
if (this.patched.length) {
parentRoot.patched.push(fiber);
}
}
/**
* Finds a child that has dom that is not yet updated, and update it. This
* method is meant to be used only in the context of repatching the dom after
* a mounted hook failed and was handled.
*/
updateDom() {
if (!this.fiber) {
return;
}
if (this.bdom === this.fiber!.bdom) {
// If the error was handled by some child component, we need to find it to
// apply its change
for (let k in this.children) {
const child = this.children[k];
child.updateDom();
}
} else {
// if we get here, this is the component that handled the error and rerendered
// itself, so we can simply patch the dom
this.bdom!.patch(this.fiber!.bdom, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
}
}
// ---------------------------------------------------------------------------
// Block DOM methods
// ---------------------------------------------------------------------------
firstNode(): Node | undefined {
const bdom = this.bdom;
return bdom ? bdom.firstNode() : undefined;
}
mount(parent: HTMLElement, anchor: ChildNode) {
const bdom = this.fiber!.bdom!;
this.bdom = bdom;
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.fiber = null;
}
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
}
patch() {
this.bdom!.patch(this!.fiber!.bdom!, false);
this.cleanOutdatedChildren();
this.fiber!.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
console.log('ddddddd')
this._destroy();
}
remove() {
console.log('coucou')
this.bdom!.remove();
}
cleanOutdatedChildren() {
const childrenEntries = Object.entries(this.children);
if (!childrenEntries.length) {
return;
}
const children = this.children;
for (const [key, node] of childrenEntries) {
const status = node.status;
if (status !== STATUS.MOUNTED) {
delete children[key];
if (status !== STATUS.DESTROYED) {
node.destroy();
}
}
}
}
}
-501
View File
@@ -1,501 +0,0 @@
import { QWeb } from "../qweb/index";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive", "asyncroot"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
}
}
}
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`);
}
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? `String(-${componentID} - i)`
: String(componentID);
if (ctx.allowMultipleRoots) {
// necessary to prevent collisions
if (!key && ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
templateID = `template${id}`;
} else {
templateID = `"_slot_${templateID}"`;
}
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
let styleAttr = node.getAttribute("style");
let tattStyle = node.getAttribute("t-att-style");
if (tattStyle) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
if (classAttr) {
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.inLoop) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
}
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
);
if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
}
ctx.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${componentID}`);
// new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
ctx.addLine(
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
if (clone.childNodes.length) {
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
let scopeVars = "";
if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
if (varDefs.length) {
scopeVars += `, {${varDefs.join(",")}}`;
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update component
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
+66
View File
@@ -0,0 +1,66 @@
import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers";
// Maps fibers to thrown errors
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
if (!node) {
return false;
}
const fiber = node.fiber;
if (fiber) {
fibersInError.set(fiber, error);
}
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let stopped = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
stopped = true;
break;
} catch (e) {
error = e;
}
}
if (stopped) {
if (isFirstRound && fiber) {
fiber.root.counter--;
}
return true;
}
}
return _handleError(node.parent, error);
}
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) {
const error = params.error;
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
fibersInError.set(fiber.root, error);
const handled = _handleError(node, error, true);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
node.app.destroy();
} catch (e) {
console.error(e);
}
}
}
+236
View File
@@ -0,0 +1,236 @@
import { BDom, mount } from "../blockdom";
import type { ComponentNode } from "./component_node";
import { fibersInError, handleError } from "./error_handling";
import { STATUS } from "./status";
/**
* Cleans on the root fiber the patch and willPatch fiber lists
* It is typically needed when the same root fiber needs to recycle on
* of its children or grandchildren's fiber.
*/
function cleanPatchableFiber(child: Fiber, root: RootFiber) {
const { willPatch, patched } = root;
let i = willPatch.indexOf(child);
if (i > -1) {
willPatch.splice(i, 1);
}
i = patched.indexOf(child);
if (i > -1) {
patched.splice(i, 1);
}
}
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
// current is necessarily a rootfiber here
let root = parent.root;
const isSameRoot = current.root === root;
cancelFibers(root, current.children);
current.children = [];
current.parent = parent;
// only increment our rendering if we were not
// already accounted for, or that we have been rendered
// already (in which case our fiber was removed from the root rendering)
if (!isSameRoot || current.bdom) {
root.counter++;
}
if (isSameRoot) {
cleanPatchableFiber(current, root);
}
current.bdom = null;
current.root = root;
return current;
}
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root;
root.counter -= cancelFibers(root, current.children);
current.children = [];
root.counter++;
current.bdom = null;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
}
return current;
}
const fiber = new RootFiber(node, null);
if (node.willPatch.length) {
fiber.willPatch.push(fiber);
}
if (node.patched.length) {
fiber.patched.push(fiber);
}
return fiber;
}
/**
* @returns number of not-yet rendered fibers cancelled
*/
function cancelFibers(root: any, fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
fiber.node.fiber = null;
fiber.root = root;
if (!fiber.bdom) {
result++;
}
result += cancelFibers(root, fiber.children);
}
return result;
}
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber;
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
this.parent = parent;
if (parent) {
const root = parent.root;
root.counter++;
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
}
export class RootFiber extends Fiber {
counter: number = 1;
// only add stuff in this if they have registered some hooks
willPatch: Fiber[] = [];
patched: Fiber[] = [];
mounted: Fiber[] = [];
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
locked: boolean = false;
complete() {
const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
// because of the asynchronous nature of the rendering, some parts of the
// UI may have been rendered, then deleted in a followup rendering, and we
// do not want to call onWillPatch in that case.
let node = current.node;
if (node.fiber === current) {
const component = node.component;
for (let cb of node.willPatch) {
cb.call(component);
}
}
}
current = undefined;
// Step 2: patching the dom
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
node.cleanOutdatedChildren();
this.appliedToDom = true;
this.locked = false;
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
// Step 5: calling all patched hooks
let patchedFibers = this.patched;
while ((current = patchedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.patched) {
cb();
}
}
}
} catch (e) {
this.locked = false;
handleError({ fiber: current || this, error: e });
}
}
}
type Position = "first-child" | "last-child";
export interface MountOptions {
position?: Position;
}
export class MountFiber extends RootFiber {
target: HTMLElement;
position: Position;
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
super(node, null);
this.target = target;
this.position = options.position || "last-child";
}
complete() {
let current: Fiber | undefined = this;
try {
const node = this.node;
if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing
// bdom, this means that this same fiber was already completed, mounted,
// but a crash occurred in some mounted hook. Then, it was handled and
// the new rendering is being applied.
node.updateDom();
} else {
node.bdom = this.bdom;
if (this.position === "last-child" || this.target.childNodes.length === 0) {
mount(node.bdom!, this.target);
} else {
const firstChild = this.target.childNodes[0];
mount(node.bdom!, this.target, firstChild);
}
}
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
node.status = STATUS.MOUNTED;
this.appliedToDom = true;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
} catch (e) {
handleError({ fiber: current as Fiber, error: e });
}
}
}
+36
View File
@@ -0,0 +1,36 @@
import { filterOutModifiersFromData } from "../blockdom/config";
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
data = _data;
let stopped = false;
if (modifiers.length) {
let selfMode = false;
const isSelf = ev.target === currentTarget;
for (const mod of modifiers) {
switch (mod) {
case "self":
selfMode = true;
if (isSelf) {
continue;
} else {
return stopped;
}
case "prevent":
if ((selfMode && isSelf) || !selfMode) ev.preventDefault();
continue;
case "stop":
if ((selfMode && isSelf) || !selfMode) ev.stopPropagation();
stopped = true;
continue;
}
}
}
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
// as expected when there is a handler expression that evaluates to a falsy value
if (Object.hasOwnProperty.call(data, 0)) {
data[0].call(data[1] ? data[1].__owl__.component : null, ev);
}
return stopped;
};
+71
View File
@@ -0,0 +1,71 @@
import { getCurrent } from "./component_node";
import { nodeErrorHandlers } from "./error_handling";
// -----------------------------------------------------------------------------
// hooks
// -----------------------------------------------------------------------------
export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willStart.push(fn);
}
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent()!;
node.willUpdateProps.push(fn);
}
export function onMounted(fn: () => void | any) {
const node = getCurrent()!;
node.mounted.push(fn);
}
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!;
node.willPatch.unshift(fn);
}
export function onPatched(fn: () => void | any) {
const node = getCurrent()!;
node.patched.push(fn);
}
export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willUnmount.unshift(fn);
}
export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willDestroy.push(fn);
}
export function onWillRender(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
fn();
return renderFn();
};
}
export function onRendered(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
fn();
return result;
};
}
type OnErrorCallback = (error: any) => void | any;
export function onError(callback: OnErrorCallback) {
const node = getCurrent()!;
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.push(callback);
}
+73 -36
View File
@@ -1,8 +1,32 @@
import { QWeb } from "../qweb/index"; import { Component } from "./component";
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
const defaultProps = (ComponentClass as any).defaultProps;
if (defaultProps) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
}
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Prop validation helper // Prop validation helper
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function getPropDescription(staticProps: any) {
if (staticProps instanceof Array) {
return Object.fromEntries(
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
);
}
return staticProps || { "*": true };
}
/** /**
* Validate the component props (or next props) against the (static) props * Validate the component props (or next props) against the (static) props
@@ -10,44 +34,44 @@ import { QWeb } from "../qweb/index";
* visit recursively the props and all the children to check if they are valid. * visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode. * This is why it is only done in 'dev' mode.
*/ */
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
const ComponentClass = (
typeof name !== "string" ? name : parent.constructor.components[name]
) as typeof Component;
QWeb.utils.validateProps = function(Widget, props: Object) { applyDefaultProps(props, ComponentClass);
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) { let propsDef = getPropDescription(ComponentClass.props);
// list of strings (prop names) const allowAdditionalProps = "*" in propsDef;
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i]; for (let propName in propsDef) {
if (propName[propName.length - 1] === "?") { if (propName === "*") {
// optional prop continue;
break; }
} if (props[propName] === undefined) {
if (!props[propName]) { if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`); throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else {
continue;
} }
} }
for (let key in props) { let isValid;
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) { try {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`); isValid = isValidProp(props[propName], propsDef[propName]);
} } catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
})`;
throw e;
} }
} else if (propsDef) { if (!isValid) {
// propsDef is an object now throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
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}'`);
}
} }
}
if (!allowAdditionalProps) {
for (let propName in props) { for (let propName in props) {
if (!(propName in propsDef)) { if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`); throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
} }
} }
} }
@@ -56,7 +80,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
/** /**
* Check if an invidual prop value matches its (static) prop definition * Check if an invidual prop value matches its (static) prop definition
*/ */
function isValidProp(prop, propDef): boolean { function isValidProp(prop: any, propDef: any): boolean {
if (propDef === true) { if (propDef === true) {
return true; return true;
} }
@@ -80,17 +104,30 @@ function isValidProp(prop, propDef): boolean {
return result; return result;
} }
// propsDef is an object // propsDef is an object
let result = isValidProp(prop, propDef.type); if (propDef.optional && prop === undefined) {
if (propDef.type === Array) { return true;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) { for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element); result = result && isValidProp(prop[i], propDef.element);
} }
} }
if (propDef.type === Object) { if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape; const shape = propDef.shape;
for (let key in shape) { for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]); result = result && isValidProp(prop[key], shape[key]);
} }
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
} }
return result; return result;
} }
+74
View File
@@ -0,0 +1,74 @@
import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status";
// -----------------------------------------------------------------------------
// Scheduler
// -----------------------------------------------------------------------------
export class Scheduler {
tasks: Set<RootFiber> = new Set();
isRunning: boolean = false;
requestAnimationFrame: Window["requestAnimationFrame"];
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
this.requestAnimationFrame = requestAnimationFrame;
}
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root);
if (!this.isRunning) {
this.start();
}
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
*/
flush() {
this.tasks.forEach((fiber) => {
if (fiber.root !== fiber) {
this.tasks.delete(fiber);
return;
}
const hasError = fibersInError.has(fiber);
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber);
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!hasError) {
fiber.complete();
}
this.tasks.delete(fiber);
}
});
if (this.tasks.size === 0) {
this.stop();
}
}
scheduleTasks() {
this.requestAnimationFrame(() => {
this.flush();
if (this.isRunning) {
this.scheduleTasks();
}
});
}
}
+24
View File
@@ -0,0 +1,24 @@
import type { Component } from "./component";
// -----------------------------------------------------------------------------
// Status
// -----------------------------------------------------------------------------
export const enum STATUS {
NEW,
MOUNTED, // is ready, and in DOM. It has a valid el
DESTROYED,
}
type STATUS_DESCR = "new" | "mounted" | "destroyed";
export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) {
case STATUS.NEW:
return "new";
case STATUS.MOUNTED:
return "mounted";
case STATUS.DESTROYED:
return "destroyed";
}
}
-79
View File
@@ -1,79 +0,0 @@
/**
* We define here a simple event bus: it can
* - emit events
* - add/remove listeners.
*
* This is a useful pattern of communication in many cases. For OWL, each
* components and stores are event buses.
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type Callback = (...args: any[]) => void;
export interface Subscription {
owner: any;
callback: Callback;
}
//------------------------------------------------------------------------------
// EventBus
//------------------------------------------------------------------------------
export class EventBus {
subscriptions: { [eventType: string]: Subscription[] } = {};
/**
* Add a listener for the 'eventType' events.
*
* Note that the 'owner' of this event can be anything, but will more likely
* be a component or a class. The idea is that the callback will be called with
* the proper owner bound.
*
* Also, the owner should be kind of unique. This will be used to remove the
* listener.
*/
on(eventType: string, owner: any, callback: Callback) {
if (!callback) {
throw new Error("Missing callback");
}
if (!this.subscriptions[eventType]) {
this.subscriptions[eventType] = [];
}
this.subscriptions[eventType].push({
owner,
callback
});
}
/**
* Remove a listener
*/
off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType];
if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
}
}
/**
* Emit an event of type 'eventType'. Any extra arguments will be passed to
* the listeners callback.
*/
trigger(eventType: string, ...args: any[]) {
const subs = this.subscriptions[eventType] || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
sub.callback.call(sub.owner, ...args);
}
}
/**
* Remove all subscriptions.
*/
clear() {
this.subscriptions = {};
}
}
-110
View File
@@ -1,110 +0,0 @@
/**
* Owl Observer
*
* This code contains the logic that allows Owl to observe and react to state
* changes.
*
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
* - Objects and arrays are observed by replacing them with a Proxy
* - each object/array metadata are tracked in a weakmap, and keep a revision
* number
*
* Note that this code is loosely inspired by Vue.
*/
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe(value: any, parent?: any): any {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
return value;
}
let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy;
}
revNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0;
}
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) {
var self = this;
const proxy = new Proxy(value, {
get(target, k) {
const targetValue = target[k];
return self.observe(targetValue, value);
},
set(target, key: string, newVal): boolean {
const value = target[key];
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
}
return true;
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
}
return true;
}
});
const metadata = {
value,
proxy,
rev: this.rev,
deepRev: this.rev,
parent
};
this.weakMap.set(value, metadata);
this.weakMap.set(metadata.proxy, metadata);
return metadata;
}
_updateRevNumber(target: any) {
this.rev++;
let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.deepRev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
+119
View File
@@ -0,0 +1,119 @@
import type { Env } from "./app/app";
import { getCurrent } from "./component/component_node";
import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks";
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
const node = getCurrent()!;
const refs = node.refs;
return {
get el(): T | null {
return refs[name] || null;
},
};
}
// -----------------------------------------------------------------------------
// useEnv and useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return getCurrent()!.component.env as any;
}
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(envExtension: Env) {
const node = getCurrent()!;
const env = Object.create(node.childEnv);
const descrs = Object.getOwnPropertyDescriptors(envExtension);
node.childEnv = Object.freeze(Object.defineProperties(env, descrs));
}
// -----------------------------------------------------------------------------
// useEffect
// -----------------------------------------------------------------------------
const NO_OP = () => {};
/**
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
type Effect = (...dependencies: any[]) => void | (() => void);
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
let cleanup: () => void;
let dependencies: any[];
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies) || NO_OP;
});
onPatched(() => {
const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
cleanup();
cleanup = effect(...dependencies) || NO_OP;
}
});
onWillUnmount(() => cleanup());
}
// -----------------------------------------------------------------------------
// useExternalListener
// -----------------------------------------------------------------------------
/**
* When a component needs to listen to DOM Events on element(s) that are not
* part of his hierarchy, we can use the `useExternalListener` hook.
* It will correctly add and remove the event listener, whenever the
* component is mounted and unmounted.
*
* Example:
* a menu needs to listen to the click on window to be closed automatically
*
* Usage:
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(
target: HTMLElement | typeof window,
eventName: string,
handler: EventListener,
eventParams?: AddEventListenerOptions
) {
const node = getCurrent()!;
const boundHandler = handler.bind(node.component);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+53 -36
View File
@@ -1,40 +1,57 @@
/** import {
* This file is the main file packaged by rollup (see rollup.config.js). From config,
* this file, we export all public owl elements. createBlock,
* html,
* Note that dynamic values, such as a date or a commit hash are added by rollup list,
*/ mount as blockMount,
import { EventBus } from "./core/event_bus"; multi,
import { Observer } from "./core/observer"; patch,
import { QWeb } from "./qweb/index"; remove,
import { ConnectedComponent } from "./store/connected_component"; text,
import { Store } from "./store/store"; toggler,
import * as _utils from "./utils"; comment,
import { Link } from "./router/Link"; } from "./blockdom";
import { Router } from "./router/Router"; import { mainEventHandler } from "./component/handler";
config.shouldNormalizeDom = false;
config.mainEventHandler = mainEventHandler;
export const blockDom = {
config,
// bdom entry points
mount: blockMount,
patch,
remove,
// bdom block types
list,
multi,
text,
toggler,
createBlock,
html,
comment,
};
export { App, mount } from "./app/app";
export { Component } from "./component/component"; export { Component } from "./component/component";
export { QWeb }; export { useComponent } from "./component/component_node";
export const core = { EventBus, Observer }; export { status } from "./component/status";
export const router = { Router, Link }; export { Memo } from "./memo";
export const store = { Store, ConnectedComponent }; export { xml } from "./app/template_set";
export const utils = _utils; export { useState, reactive } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onWillPatch,
onPatched,
onWillRender,
onRendered,
onWillDestroy,
onError,
} from "./component/lifecycle_hooks";
export const __info__ = {}; 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.`);
}
}
});
+47
View File
@@ -0,0 +1,47 @@
import { Component } from "./component/component";
import type { ComponentNode } from "./component/component_node";
import { xml } from "./app/template_set";
import { Fiber } from "./component/fibers";
export class Memo extends Component {
static template = xml`<t t-slot="default"/>`;
constructor(props: any, env: any, node: ComponentNode) {
super(props, env, node);
// prevent patching process conditionally
let applyPatch = false;
const patchFn = node.patch;
node.patch = () => {
if (applyPatch) {
patchFn.call(node);
applyPatch = false;
}
};
// check props change, and render/apply patch if it changed
let prevProps = props;
const updateAndRender = node.updateAndRender;
node.updateAndRender = function (props: any, parentFiber: Fiber) {
const shouldUpdate = !shallowEqual(prevProps, props);
if (shouldUpdate) {
prevProps = props;
updateAndRender.call(node, props, parentFiber);
applyPatch = true;
}
return Promise.resolve();
};
}
}
/**
* we assume that each object have the same set of keys
*/
function shallowEqual(p1: any, p2: any): boolean {
for (let k in p1) {
if (k !== "slots" && p1[k] !== p2[k]) {
return false;
}
}
return true;
}
+51
View File
@@ -0,0 +1,51 @@
import { BDom, text, VNode } from "./blockdom";
const VText: any = text("").constructor;
export class VPortal extends VText implements Partial<VNode<VPortal>> {
// selector: string;
realBDom: BDom | null;
target: HTMLElement | null = null;
constructor(selector: string, realBDom: BDom, ownerComponent: ComponentNode) {
super("");
this.ownerComponent = ownerComponent;
this.selector = selector;
this.realBDom = realBDom;
}
mount(parent: HTMLElement, anchor: ChildNode) {
super.mount(parent, anchor);
this.ownerComponent.willDestroy.push(() => this.cleanup());
this.target = document.querySelector(this.selector) as any;
if (!this.target) {
let el: any = this.el;
while (el && el.parentElement instanceof HTMLElement) {
el = el.parentElement;
}
this.target = el && el.querySelector(this.selector);
if (!this.target) {
throw new Error("invalid portal target");
}
}
this.realBDom!.mount(this.target!, null);
}
beforeRemove() {
this.realBDom!.beforeRemove();
}
remove() {
super.remove();
this.realBDom!.remove();
this.realBDom = null;
}
patch(other: VPortal) {
super.patch(other);
if (this.realBDom) {
this.realBDom.patch(other.realBDom!, true);
} else {
this.realBDom = other.realBDom;
this.realBDom!.mount(this.target!, null);
}
}
}
-320
View File
@@ -1,320 +0,0 @@
import { Context } from "./context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
/**
* Owl QWeb Directives
*
* This file contains the implementation of most standard QWeb directives:
* - t-esc
* - t-raw
* - t-set/t-value
* - t-if/t-elif/t-else
* - t-call
* - t-foreach/t-as
* - t-debug
* - t-log
*/
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
if (value === "0" && ctx.caller) {
qweb._compileNode(ctx.caller, ctx);
return;
}
if (value.xml instanceof NodeList) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
}
} else {
let fragID = ctx.generateID();
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
}
QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
}
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
}
});
QWeb.addDirective({
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
}
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
}
});
//------------------------------------------------------------------------------
// t-set
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
}
return true;
}
});
//------------------------------------------------------------------------------
// t-if, t-elif, t-else
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "if",
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(`${ctx.formatExpression(cond)}`);
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "elif",
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
QWeb.addDirective({
name: "else",
priority: 40,
atNodeEncounter({ ctx }): boolean {
ctx.addLine(`else {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
}
});
//------------------------------------------------------------------------------
// t-call
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!;
const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new Context();
tempCtx.nextID = ctx.rootContext.nextID;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
ctx.rootContext.nextID = tempCtx.nextID;
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
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...
}
}
// compile sub template
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
ctx.dedent();
ctx.addLine("}");
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
//------------------------------------------------------------------------------
// t-foreach
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "foreach",
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx = ctx.subContext("inLoop", true);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addToScope(name + "_first", "i === 0");
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
ctx.addToScope(name + "_index", "i");
ctx.addToScope(name, `_${keysID}[i]`);
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (
!shouldWarn &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key")
) {
shouldWarn = true;
}
}
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${
ctx.templateName
}')`
);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
return true;
}
});
//------------------------------------------------------------------------------
// t-debug
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
});
//------------------------------------------------------------------------------
// t-log
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
}
});
-167
View File
@@ -1,167 +0,0 @@
import { compileExpr, QWebVar } from "./expression_parser";
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineResult: boolean = false;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
}
withParent(node: number): Context {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
return compileExpr(expr, this.variables);
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
-284
View File
@@ -1,284 +0,0 @@
import { VNode } from "../vdom/index";
import { QWeb } from "./qweb";
/**
* Owl QWeb Extensions
*
* This file contains the implementation of non standard QWeb directives, added
* by Owl and that will only work on Owl projects:
*
* - t-on
* - t-ref
* - t-transition
* - t-mounted
* - t-slot
* - t-model
*/
//------------------------------------------------------------------------------
// t-on
//------------------------------------------------------------------------------
// these are pieces of code that will be injected into the event handler if
// modifiers are specified
export const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
};
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
whenTransitionEnd(elm, finalize);
});
};
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
whenTransitionEnd(elm, finalize);
});
};
function getTimeout(delays: Array<string>, durations: Array<string>): number {
/* istanbul ignore next */
while (delays.length < durations.length) {
delays = delays.concat(delays);
}
return Math.max.apply(
null,
durations.map((d, i) => {
return toMs(d) + toMs(delays[i]);
})
);
}
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
function toMs(s: string): number {
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
}
function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}, vars: extra.vars, parent: owner}));`
);
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
-4
View File
@@ -1,4 +0,0 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
-695
View File
@@ -1,695 +0,0 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context";
/**
* Owl QWeb Engine
*
* In this file, you will find a QWeb engine/template compiler. It is the core
* of how Owl component works.
*
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
* DOM representation.
*
* We have here:
* - a CompilationContext class, which is an internal object that contains all
* compilation specific information, while a template is being compiled.
* - a QWeb class: this is the code of the QWeb compiler.
*
* Note that this file does not contain the implementation of the QWeb
* directives (see qweb_directives.ts and qweb_extensions.ts).
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type EvalContext = { [key: string]: any };
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
interface Template {
elem: Element;
fn: CompiledTemplate;
}
interface CompilationInfo {
node: Element;
qweb: QWeb;
ctx: Context;
fullName: string;
value: string;
}
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive {
name: string;
extraNames?: string[];
priority: number;
// if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void;
}
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)"
};
interface Utils {
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
const UTILS: Utils = {
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
};
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;
}
let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template } = {};
static utils = UTILS;
static components = Object.create(null);
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
name: 1,
att: 1,
attf: 1,
key: 1
};
static DIRECTIVES: Directive[] = [];
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
// the id field is useful to be able to hash qweb instances. The current
// use case is that component's templates are qweb dependant, and need to be
// able to map a qweb instance to a template name.
id = nextID++;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
slots = {};
nextSlotId = 1;
isUpdating: boolean = false;
constructor(data?: string) {
super();
if (data) {
this.addTemplates(data);
}
}
static addDirective(directive: Directive) {
if (directive.name in QWeb.DIRECTIVE_NAMES) {
throw new Error(`Directive "${directive.name} already registered`);
}
QWeb.DIRECTIVES.push(directive);
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
static register(name: string, Component: any) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.components[name] = Component;
}
/**
* 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: (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 = {}): string {
const vnode = this.render(name, context);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
}
/**
* Force all widgets connected to this QWeb instance to rerender themselves.
*
* This method is mostly useful for external code that want to modify the
* application in some cases. For example, a router plugin.
*/
forceUpdate() {
this.isUpdating = true;
Promise.resolve().then(() => {
if (this.isUpdating) {
this.isUpdating = false;
this.trigger("update");
}
});
}
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
if (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);");
}
this._compileNode(elem, ctx);
if (!parentContext) {
if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`);
} else {
if (!ctx.rootNode) {
throw new Error(`A template should have one root node (${ctx.templateName})`);
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: Context) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
let withHandlers = false;
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
}
}
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
for (let i = 0; i < DIR_N; i++) {
let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value
});
if (isDone) {
return;
}
}
}
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
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);
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value });
}
}
}
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
const formattedExpr = ctx.interpolate(value);
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`var _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
nodeKey = ctx.formatExpression(nodeKey);
} else {
nodeKey = nodeID;
}
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
ctx.addElse();
ctx.addLine(`for (let key in _${id}) {`);
ctx.indent();
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
ctx.dedent();
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: Context) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
+241
View File
@@ -0,0 +1,241 @@
import { onWillUnmount } from "./component/lifecycle_hooks";
import { ComponentNode, getCurrent } from "./component/component_node";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
const TARGET = Symbol("Target");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
type ObjectKey = string | number | symbol;
type Target = object;
type Callback = () => void;
type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
/**
* Checks whether a given value can be made into a reactive object.
*
* @param value the value to check
* @returns whether the value can be made reactive
*/
function canBeMadeReactive(value: any): boolean {
return (
typeof value === "object" &&
value !== null &&
!(value instanceof Date) &&
!(value instanceof Promise) &&
!(value instanceof String) &&
!(value instanceof Number)
);
}
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
/**
* Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target.
*
* @param target the target whose key should be observed
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
* or deletion)
* @param callback the function to call when the key changes
*/
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
keyToCallbacks.get(key)!.add(callback);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
/**
* Notify Reactives that are observing a given target that a key has changed on
* the target.
*
* @param target target whose Reactives should be notified that the target was
* changed.
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted)
*/
function notifyReactives(target: Target, key: ObjectKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToCallbacks) {
return;
}
const callbacks = keyToCallbacks.get(key);
if (!callbacks) {
return;
}
// Loop on copy because clearReactivesForCallback will modify the set in place
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
}
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
callbacks.delete(callback);
}
}
targetsToClear.clear();
}
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
/**
* Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the
* notify callback to be called if there are suscriptions to that data. Nested
* objects and arrays are automatically made reactive as well.
*
* Whenever you are notified of a change, all subscriptions are cleared, and if
* you would like to be notified of any further changes, you should go read
* the underlying data again. We assume that if you don't go read it again after
* being notified, it means that you are no longer interested in that data.
*
* Subscriptions:
* + Reading a property on an object will subscribe you to changes in the value
* of that property.
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
* subscribe you to the creation/deletion of keys. Checking the presence of a
* key on the object with 'in' has the same effect.
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
* This is a choice that was made because changing a key's value will trigger
* this trap and we do not want to subscribe by writes. This also means that
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
*
* @param target the object for which to create a reactive proxy
* @param callback the function to call when an observed property of the
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): Reactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new Map());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const proxy = new Proxy(target, {
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
if (key === TARGET) {
return target;
}
observeTargetKey(target, key, callback);
const value = Reflect.get(target, key, proxy);
if (!canBeMadeReactive(value)) {
return value;
}
return reactive(value, callback);
},
set(target, key, value, proxy) {
const isNewKey = !Object.hasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, proxy);
const ret = Reflect.set(target, key, value, proxy);
if (isNewKey) {
notifyReactives(target, KEYCHANGES);
}
// While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the
// new value. We disable the "same-value-optimization" for it because of that.
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
notifyReactives(target, key);
}
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
notifyReactives(target, KEYCHANGES);
notifyReactives(target, key);
return ret;
},
ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.ownKeys(target);
},
has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key);
},
});
reactivesForTarget.set(callback, proxy);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
}
};
}
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = getCurrent()!;
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillUnmount(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
return reactiveState;
}
-48
View File
@@ -1,48 +0,0 @@
import { Component } from "../component/component";
import { Destination, RouterEnv } from "./Router";
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
export const LINK_TEMPLATE = `
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>`;
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
template = LINK_TEMPLATE_NAME;
href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) {
this.href = this.env.router.destToPath(nextProps);
}
get isActive() {
if (this.env.router.mode === "hash") {
return (<any>document.location).hash === this.href;
}
return (<any>document.location).pathname === this.href;
}
navigate(ev) {
// don't redirect with control keys
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
// don't redirect on right click
if (ev.button !== undefined && ev.button !== 0) {
return;
}
// don't redirect if `target="_blank"`
if (ev.currentTarget && ev.currentTarget.getAttribute) {
const target = ev.currentTarget.getAttribute("target");
if (/\b_blank\b/i.test(target)) {
return;
}
}
ev.preventDefault();
this.env.router.navigate(this.props);
}
}
-262
View File
@@ -1,262 +0,0 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { makeDirective } from "./directive";
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
type NavigationGuard = (info: {
env: Env;
to: Route | null;
from: Route | null;
}) => boolean | Destination;
export interface Route {
name: string;
path: string;
component?: any;
redirect?: Destination;
params: string[];
beforeRouteEnter?: NavigationGuard;
}
export type RouteParams = { [key: string]: string | number };
export interface RouterEnv extends Env {
router: Router;
}
export interface Destination {
path?: string;
to?: string;
params?: RouteParams;
}
interface PositiveMatchResult {
type: "match";
route: Route;
params: RouteParams;
}
interface NegativeMatchResult {
type: "nomatch";
}
interface CancelledMatch {
type: "cancelled";
}
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
interface Options {
mode: Router["mode"];
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
currentRoute: Route | null = null;
currentParams: RouteParams | null = null;
mode: "history" | "hash";
routes: { [id: string]: Route };
routeIds: string[];
env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
this.routes = {};
this.routeIds = [];
let nextId = 1;
for (let partialRoute of routes) {
if (!partialRoute.name) {
partialRoute.name = "__route__" + nextId++;
}
if (partialRoute.component) {
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
}
if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
window.addEventListener("popstate", (this as any)._listener);
// setup link and directive
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
QWeb.addDirective(makeDirective(<RouterEnv>env));
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") {
this.currentRoute = result.route;
this.currentParams = result.params;
const currentPath = this.routeToPath(result.route, result.params);
if (currentPath !== this.currentPath()) {
this.setUrlFromPath(currentPath);
}
}
}
async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to);
const initialName = this.currentRouteName;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
this.setUrlFromPath(finalPath);
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
if (this.currentRouteName !== initialName) {
this.env.qweb.forceUpdate();
return true;
}
return false;
}
destToPath(dest: Destination): string {
this.validateDestination(dest);
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
}
get currentRouteName(): string | null {
return this.currentRoute && this.currentRoute.name;
}
//--------------------------------------------------------------------------
// Private helpers
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const url = location.origin + path;
window.history.pushState({}, path, url);
}
private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
}
}
private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
return parts.join("/");
}
private currentPath(): string {
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
return result || "/";
}
private match(path: string): MatchResult {
for (let routeId of this.routeIds) {
let route = this.routes[routeId];
let params = this.getRouteParams(route, path);
if (params) {
return {
type: "match",
route: route,
params: params
};
}
}
return { type: "nomatch" };
}
private async matchAndApplyRules(path: string): Promise<MatchResult> {
const result = this.match(path);
if (result.type === "match") {
return this.applyRules(result);
}
return result;
}
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
const route = matchResult.route;
if (route.redirect) {
const path = this.destToPath(route.redirect);
return this.matchAndApplyRules(path);
}
if (route.beforeRouteEnter) {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route
});
if (result === false) {
return { type: "cancelled" };
} else if (result !== true) {
// we want to navigate to another destination
const path = this.destToPath(result);
return this.matchAndApplyRules(path);
}
}
return matchResult;
}
private getRouteParams(route: Route, path: string): RouteParams | false {
if (route.path === "*") {
return {};
}
const descrParts = route.path.split("/");
const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false;
}
const result = {};
for (let i = 0; i < l; i++) {
const descr = descrParts[i];
let target: string | number = targetParts[i];
const match = descr.match(paramRegexp);
if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
}
}
return result;
}
}
function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1].split(".")[0]);
}
} while (m);
return result;
}
-28
View File
@@ -1,28 +0,0 @@
import { RouterEnv } from "./Router";
export function makeDirective(env: RouterEnv) {
return {
name: "routecomponent",
priority: 13,
atNodeEncounter({ node }): boolean {
let first = true;
const router = env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
// make new t t-component element
const comp = node.ownerDocument.createElement("t");
comp.setAttribute("t-component", "__component__" + route.name);
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
first = false;
for (let param of route.params) {
comp.setAttribute(param, `env.router.currentParams.${param}`);
}
node.appendChild(comp);
}
}
node.removeAttribute("t-routecomponent");
return false;
}
};
}
-119
View File
@@ -1,119 +0,0 @@
import { Component, Env } from "../component/component";
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
deep: boolean = true;
getStore(env) {
return env.store;
}
storeProps: any;
hashFunction: HashFunction = (storeProps, options) => {
const revFn = (this.__owl__ as any).revFn;
const rev = revFn(storeProps);
if (rev > 0) {
return rev;
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = revFn(val);
if (hashVal === 0) {
if (val !== options.prevStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
dispatch(name, ...payload) {
return (this.__owl__ as any).store.dispatch(name, ...payload);
}
/**
* Need to do this here so 'deep' can be overrided by subcomponent easily
*/
async __prepareAndRender(
scope?: Object,
vars?: any
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
const store = this.getStore(this.env);
const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
(this.__owl__ as any).store = store;
(this.__owl__ as any).revFn = revFn.bind(observer);
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps
});
(this.__owl__ as any).rev = observer.rev;
return super.__prepareAndRender(scope, vars);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
__callWillUnmount() {
(this.__owl__ as any).store.off("update", this);
super.__callWillUnmount();
}
__destroy(parent: any) {
(this.__owl__ as any).store.off("update", this);
super.__destroy(parent);
}
async __updateProps(nextProps: P, f, p, s, v) {
this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, p, s, v);
}
__updateStoreProps(nextProps): boolean {
const store = (this.__owl__ as any).store;
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
nextProps,
store.getters
);
const options = { prevStoreProps: this.storeProps, didChange: false };
const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps;
let didChange = options.didChange;
if (storeHash !== (this.__owl__ as any).storeHash) {
(this.__owl__ as any).storeHash = storeHash;
didChange = true;
}
(this.__owl__ as any).rev = store.observer.rev;
return didChange;
}
async __checkUpdate() {
const observer = (this.__owl__ as any).store.observer;
if (observer.rev === (this.__owl__ as any).rev) {
// update was already done by updateProps, from parent
return;
}
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
}
}
}
-107
View File
@@ -1,107 +0,0 @@
import { Env } from "../component/component";
import { EventBus } from "../core/event_bus";
import { Observer } from "../core/observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - the ConnectedComponent class
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.actions = config.actions;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(config.state || {});
this.getters = {};
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters,
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner);
}
}
}
}
+23 -82
View File
@@ -1,97 +1,38 @@
/** export class EventBus extends EventTarget {
* Owl Utils trigger(name: string, payload?: any) {
* this.dispatchEvent(new CustomEvent(name, { detail: payload }));
* We have here a small collection of utility functions: }
* }
* - whenReady
* - loadJS
* - loadTemplates
* - escape
* - debounce
*/
export function whenReady(fn?: any) { export function whenReady(fn?: any): Promise<void> {
return new Promise(function(resolve) { return new Promise(function (resolve) {
if (document.readyState !== "loading") { if (document.readyState !== "loading") {
resolve(); resolve(true);
} else { } else {
document.addEventListener("DOMContentLoaded", resolve, false); document.addEventListener("DOMContentLoaded", resolve, false);
} }
}).then(fn || function() {}); }).then(fn || function () {});
} }
const loadedScripts: { [key: string]: Promise<void> } = {}; export async function loadFile(url: string): Promise<string> {
export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise: Promise<void> = new Promise(function(resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
resolve();
};
script.onerror = function() {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}
export async function loadTemplates(url: string): Promise<string> {
const result = await fetch(url); const result = await fetch(url);
if (!result.ok) { if (!result.ok) {
throw new Error("Error while fetching xml templates"); throw new Error("Error while fetching xml templates");
} }
let templates = await result.text(); return await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
} }
export function escape(str: string | number | undefined): string { /*
if (str === undefined) { * This class just transports the fact that a string is safe
return ""; * to be injected as HTML. Overriding a JS primitive is quite painful though
} * so we need to redfine toString and valueOf.
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 { export class Markup extends String {}
let timeout;
return function(this: any) { /*
const context = this; * Marks a value as safe, that is, a value that can be injected as HTML directly.
const args = arguments; * It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
function later() { */
timeout = null; export function markup(value: any) {
if (!immediate) { return new Markup(value);
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
} }
-9
View File
@@ -1,9 +0,0 @@
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
@@ -1,234 +0,0 @@
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;
-624
View File
@@ -1,624 +0,0 @@
/**
* 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;
}
}
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
//------------------------------------------------------------------------------
type Key = string | number;
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
on?: On;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
[key: string]: any; // for any other 3rd party module
}
function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
//------------------------------------------------------------------------------
// snabbdom.ts
//------------------------------------------------------------------------------
function isUndef(s: any): boolean {
return s === undefined;
}
function isDef(s: any): boolean {
return s !== undefined;
}
type VNodeQueue = Array<VNode>;
const emptyNode = vnode("", {}, [], undefined, undefined);
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
type KeyToIndexMap = { [key: string]: number };
type ArraysOf<T> = { [K in keyof T]: (T[K])[] };
type ModuleHooks = ArraysOf<Module>;
function createKeyToOldIdx(
children: Array<VNode>,
beginIdx: number,
endIdx: number
): KeyToIndexMap {
let i: number,
map: KeyToIndexMap = {},
key: Key | undefined,
ch;
for (i = beginIdx; i <= endIdx; ++i) {
ch = children[i];
if (ch != null) {
key = ch.key;
if (key !== undefined) map[key] = i;
}
}
return map;
}
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number,
j: number,
cbs = {} as ModuleHooks;
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}
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);
}
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
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))) {
i(vnode);
data = vnode.data;
}
}
let children = vnode.children,
sel = vnode.sel;
if (sel === "!") {
if (isUndef(vnode.text)) {
vnode.text = "";
}
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, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
i = (vnode.data as VNodeData).hook; // Reuse variable
if (isDef(i)) {
if (i.create) i.create(emptyNode, vnode);
if (i.insert) insertedVnodeQueue.push(vnode);
}
} else {
vnode.elm = api.createTextNode(vnode.text as string);
}
return vnode.elm;
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
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, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
}
}
}
}
}
function removeVnodes(
parentElm: Node,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let i: any,
iLen: number,
listeners: number,
rm: () => void,
ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
i(ch, rm);
} else {
rm();
}
} else {
// Text node
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}
function updateChildren(
parentElm: Node,
oldCh: Array<VNode>,
newCh: Array<VNode>,
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0,
newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm as Node,
api.nextSibling(oldEndVnode.elm as Node)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
newStartVnode = newCh[++newStartIdx];
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
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);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
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);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
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);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(
elm,
null,
ch as Array<VNode>,
0,
(ch as Array<VNode>).length - 1,
insertedVnodeQueue
);
} else if (isDef(oldCh)) {
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);
}
api.setTextContent(elm, vnode.text as string);
}
if (isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode);
}
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, iLen: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
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, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
return vnode;
};
}
//------------------------------------------------------------------------------
// is.ts
//------------------------------------------------------------------------------
const array = Array.isArray;
function primitive(s: any): s is string | number {
return typeof s === "string" || typeof s === "number";
}
//------------------------------------------------------------------------------
// htmldomapi.ts
//------------------------------------------------------------------------------
interface DOMAPI {
createElement: (tagName: any) => HTMLElement;
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
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;
}
function createElement(tagName: any): HTMLElement {
return document.createElement(tagName);
}
function createElementNS(namespaceURI: string, qualifiedName: string): Element {
return document.createElementNS(namespaceURI, qualifiedName);
}
function createTextNode(text: string): Text {
return document.createTextNode(text);
}
function createComment(text: string): Comment {
return document.createComment(text);
}
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode);
}
function removeChild(node: Node, child: Node): void {
node.removeChild(child);
}
function appendChild(node: Node, child: Node): void {
node.appendChild(child);
}
function parentNode(node: Node): Node | null {
return node.parentNode;
}
function nextSibling(node: Node): Node | null {
return node.nextSibling;
}
function tagName(elm: Element): string {
return elm.tagName;
}
function setTextContent(node: Node, text: string | null): void {
node.textContent = text;
}
const htmlDomApi = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
} as DOMAPI;
//------------------------------------------------------------------------------
// hooks.ts
//------------------------------------------------------------------------------
type PreHook = () => any;
type InitHook = (vNode: VNode) => any;
type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
type InsertHook = (vNode: VNode) => any;
type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
type DestroyHook = (vNode: VNode) => any;
type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
type PostHook = () => any;
interface Hooks {
pre?: PreHook;
init?: InitHook;
create?: CreateHook;
insert?: InsertHook;
prepatch?: PrePatchHook;
update?: UpdateHook;
postpatch?: PostPatchHook;
destroy?: DestroyHook;
remove?: RemoveHook;
post?: PostHook;
}
//------------------------------------------------------------------------------
// h.ts
//------------------------------------------------------------------------------
type VNodes = Array<VNode>;
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 {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {},
children: any,
text: any,
i: number,
iLen: number;
if (c !== undefined) {
data = b;
if (array(c)) {
children = c;
} else if (primitive(c)) {
text = c;
} else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined) {
if (array(b)) {
children = b;
} else if (primitive(b)) {
text = b;
} else if (b && b.sel) {
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i]))
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
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);
}
-131
View File
@@ -1,131 +0,0 @@
// 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;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`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;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
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;
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;
}"
`;
+384
View File
@@ -0,0 +1,384 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].flag) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState destroyed component is inactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].flag) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component is inactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj1'].a;
let txt2 = ctx['contextObj2'].b;
return block1([txt1, txt2]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['contextObj'].b;
return block1([txt1], [b2]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let d2 = ctx['contextObj'].b;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].b;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
return block1([d1], [b2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two components can subscribe to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Parent\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`id\`] = v_block2[i1];
let key1 = ctx['id'];
c_block2[i1] = withKey(component(\`Quantity\`, {id: ctx['id']}, key + \`__1__\${key1}\`, node, ctx), key1);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let txt1 = ctx['total'];
let txt2 = Object.keys(ctx['state']).length;
return block1([txt1, txt2], [b2]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['state'].quantity;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState very simple use, with initial value 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
-404
View File
@@ -1,404 +0,0 @@
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextTick,
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, 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 = { 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 = { 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(widget.refs.span).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 Parent extends Widget {
components = { Child: Child };
}
class Child extends Widget {}
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 Parent extends Widget {
components = { Child: Child };
state = { display: true };
}
class Child extends Widget {}
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 combined with t-mounted", async () => {
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { flag: false };
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<span t-if="state.flag" t-transition="chimay">blue</span>
</div>
</templates>`
);
class Parent extends Widget {
constructor(parent) {
super(parent);
this.state = { flag: false };
}
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent(env);
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
let def = makeDeferred();
let phase = "enter";
patchNextFrame(cb => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
// click display the span
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
button!.click();
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>
<span t-name="Child">blue</span>
</templates>`
);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
constructor(parent) {
super(parent);
this.state = { flag: false };
}
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent(env);
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
let def = makeDeferred();
let phase = "enter";
patchNextFrame(cb => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
// click display the span
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
button!.click();
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
});
+43
View File
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app App supports env with getters/setters 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].someVal;
let txt2 = Object.keys(ctx['env'].services);
return block1([txt1, txt2]);
}
}"
`;
exports[`app can configure an app with props 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].value;
return block1([txt1]);
}
}"
`;
exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
+62
View File
@@ -0,0 +1,62 @@
import { App, Component, xml } from "../../src";
import { status } from "../../src/component/status";
import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("app", () => {
test("destroy remove the widget from the DOM", async () => {
class SomeComponent extends Component {
static template = xml`<div/>`;
}
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
const el = elem(comp);
expect(document.contains(el)).toBe(true);
app.destroy();
expect(document.contains(el)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
test("App supports env with getters/setters", async () => {
let someVal = "maggot";
const services: any = { serv1: "" };
const env = {
get someVal() {
return someVal;
},
services,
};
class SomeComponent extends Component {
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="Object.keys(env.services)" /></div>`;
}
const app = new App(SomeComponent, { env });
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>maggot serv1</div>");
someVal = "brain";
services.serv2 = "";
comp.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain serv1,serv2</div>");
});
test("can configure an app with props", async () => {
class SomeComponent extends Component {
static template = xml`<div t-esc="props.value"/>`;
}
const app = new App(SomeComponent, { props: { value: 333 } });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>333</div>");
});
});
+297
View File
@@ -0,0 +1,297 @@
import { createBlock, mount, patch, remove, text } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("adding/patching blocks", () => {
test("simple block", async () => {
const block = createBlock("<div>foo</div>");
const tree = block();
expect(tree.el).toBe(undefined);
mount(tree, fixture);
expect(tree.el).not.toBe(undefined);
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("block with dynamic content", async () => {
const block = createBlock("<div><p><block-text-0/></p></div>");
const tree = block(["foo"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
patch(tree, block(["bar"]));
expect(fixture.innerHTML).toBe("<div><p>bar</p></div>");
patch(tree, block(["foo"]));
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
});
test("block with 2 dynamic text nodes", async () => {
const block = createBlock("<div><p><block-text-0/></p><span><block-text-1/></span></div>");
const tree = block(["foo", "bar"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>foo</p><span>bar</span></div>");
patch(tree, block(["appa", "yip yip"]));
expect(fixture.innerHTML).toBe("<div><p>appa</p><span>yip yip</span></div>");
});
test("block with multiple references", async () => {
const block1 = createBlock(
"<div><block-text-0/><p><block-text-1/><block-text-2/></p><block-text-3/></div>"
);
const tree = block1(["1", "2", "3", "4"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div>1<p>23</p>4</div>");
});
test("falsy values in block nodes", () => {
const cases = [
[false, "false"],
[undefined, ""],
[null, ""],
[0, "0"],
["", ""],
];
const block = createBlock("<p><block-text-0/></p>");
for (let [value, result] of cases) {
const fixture = makeTestFixture();
mount(block([value as any]), fixture);
expect(fixture.innerHTML).toBe(`<p>${result}</p>`);
}
});
});
describe("sub blocks", () => {
test("block with subblock (only child)", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<p>yip yip</p>");
const tree = block1([], [block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>yip yip</p></div>");
});
test("block with subblock (first child with sibling)", async () => {
const block1 = createBlock("<div><block-child-0/><span>something</span></div>");
const block2 = createBlock("<p>yip yip</p>");
const tree = block1([], [block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>yip yip</p><span>something</span></div>");
});
test("block with subblock (last child with sibling)", async () => {
const block1 = createBlock("<div><span>something</span><block-child-0/></div>");
const block2 = createBlock("<p>yip yip</p>");
const tree = block1([], [block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><span>something</span><p>yip yip</p></div>");
});
test("block with 2 subblocks", async () => {
const block1 = createBlock("<div><block-child-0/><block-child-1/></div>");
const block2 = createBlock("<p>yip yip</p>");
const tree = block1([], [block2(), text("appa")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>yip yip</p>appa</div>");
});
test("block with subblock with siblings", async () => {
const block1 = createBlock("<div><p>1</p><block-child-0/><p>2</p></div>");
const block2 = createBlock("<p>yip yip</p>");
const tree = block1([], [block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p><p>yip yip</p><p>2</p></div>");
});
test("block with text, subblock and siblings", async () => {
let block1 = createBlock(`<div><p>before<block-child-0/>after</p><block-child-1/></div>`);
let tree = block1([], [text("water"), text("fire")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>beforewaterafter</p>fire</div>");
});
test("block with conditional child", async () => {
const block1 = createBlock("<div><p><block-child-0/></p></div>");
const block2 = createBlock("<span>foo</span>");
const tree = block1();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p></p></div>");
patch(tree, block1([], [block2()]));
expect(fixture.innerHTML).toBe("<div><p><span>foo</span></p></div>");
patch(tree, block1());
expect(fixture.innerHTML).toBe("<div><p></p></div>");
});
test("block with subblock with dynamic content", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<p><block-text-0/></p>");
const tree = block1([], [block2(["yip yip"])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>yip yip</p></div>");
patch(tree, block1([], [block2(["foo"])]));
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
});
test("block with dynamic content and subblock", async () => {
const block1 = createBlock("<div><block-child-0/><p><block-text-0/></p></div>");
const block2 = createBlock("<p>sub block</p>");
const tree = block1(["yip yip"], [block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>sub block</p><p>yip yip</p></div>");
patch(tree, block1(["foo"], [block2()]));
expect(fixture.innerHTML).toBe("<div><p>sub block</p><p>foo</p></div>");
});
});
describe("remove elem blocks", () => {
test("elem block can be removed", async () => {
const block = createBlock("<div>foo</div>");
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div>foo</div>");
remove(tree);
expect(fixture.innerHTML).toBe("");
expect(fixture.childNodes.length).toBe(0);
});
});
describe("misc", () => {
test("constructed block as correct number of refs", () => {
const block = createBlock("<p><p><block-text-0/></p><block-text-1/></p>");
const tree = block(["a", "b"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p><p>a</p>b</p>");
expect((tree as any).refs.length).toBe(2);
});
test("block vnode can be used as text", () => {
const block = createBlock("<p>a</p>");
mount(text(block() as any), fixture);
expect(fixture.textContent).toBe("<p>a</p>");
});
test("block vnode can be used to represent a <tr>", () => {
const block = createBlock("<tr><td>tomato</td></tr>");
const tree = block();
const fixture = document.createElement("table");
mount(tree, fixture);
expect(fixture.outerHTML).toBe("<table><tr><td>tomato</td></tr></table>");
});
test("block vnode with <tr> can be used as text ", () => {
const block = createBlock("<tr><td>tomato</td></tr>");
mount(text(block() as any), fixture);
expect(fixture.textContent).toBe("<tr><td>tomato</td></tr>");
});
test("call toString for function/objects if used as inline text in block", () => {
const block = createBlock("<p><block-text-0/><block-text-1/></p>");
const f = () => 3;
const g = () => 4;
g.toString = () => "tostring";
mount(block([f, g]), fixture);
expect(fixture.innerHTML).toBe("<p>() =&gt; 3tostring</p>");
});
test("block with 2 subblocks: variation", async () => {
const block = createBlock("<a><b><c><block-child-0/>2</c></b><block-child-1/></a>");
const tree = block([], [text("1"), text("3")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<a><b><c>12</c></b>3</a>");
});
test("block with 2 subblocks: another variation", async () => {
const block = createBlock(
`<a block-attribute-0="hello"><b><c><block-child-0/>2</c></b><block-child-1/></a>`
);
const tree = block(["world"], [text("1"), text("3")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<a hello="world"><b><c>12</c></b>3</a>`);
});
test("namespace is not propagated to siblings", () => {
const block = createBlock(`<div><svg block-ns="someNameSpace"><g/></svg><div></div></div>`);
const fixture = makeTestFixture();
mount(block(), fixture);
expect(fixture.innerHTML).toBe("<div><svg><g></g></svg><div></div></div>");
expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace");
expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace");
const allDivs = fixture.querySelectorAll("div");
expect(Array.from(allDivs).map((el) => el.namespaceURI)).toEqual([
"http://www.w3.org/1999/xhtml",
"http://www.w3.org/1999/xhtml",
]);
});
// test.skip("reusing a block skips patching process", async () => {
// const block = createBlock('<div><block-text-0/></div>');
// const foo = block(["foo"]);
// const bar = block(["bar"]);
// let fooCounter = 0;
// let barCounter = 0;
// let fooValue = "foo";
// let barValue = "bar";
// Object.defineProperty(foo.data, 0, {
// get() {
// fooCounter++;
// return fooValue;
// },
// });
// Object.defineProperty(bar.data, 0, {
// get() {
// barCounter++;
// return barValue;
// },
// set(val) {
// barValue = val;
// },
// });
// const bdom = multi([foo, bar]);
// mount(bdom, fixture);
// expect(fooCounter).toBe(1);
// expect(barCounter).toBe(1);
// expect(fixture.innerHTML).toBe("<div>foo</div><div>bar</div>");
// patch(bdom, multi([foo, block(["otherbar"])]));
// expect(fixture.innerHTML).toBe("foootherbar");
// expect(fooCounter).toBe(1);
// expect(barCounter).toBe(2);
// });
});

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