Compare commits

...

86 Commits

Author SHA1 Message Date
Géry Debongnie b1e79677cb wip 2021-11-20 10:45:30 +01:00
Géry Debongnie 89d6b8b7dd wip 2021-11-20 10:25:37 +01:00
Géry Debongnie 9c2523e3ce wip 2021-11-20 10:25:37 +01:00
Géry Debongnie d8201b8955 add possibility to deep rendering 2021-11-20 10:25:37 +01:00
Géry Debongnie dfead2836e big change: shallow render
With this commit, component only render child
components if they have different props (shallow
equality). Otherwise, we trust the reactivity
system to make sure that all impacted components
are updated
2021-11-20 10:25:37 +01:00
Géry Debongnie 0e0ac5329a [REF] utils: move batched from reactivity to utils 2021-11-20 10:25:36 +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
237 changed files with 43382 additions and 33316 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ name: Node.js CI
on:
pull_request:
branches: [ master ]
branches: [ master, owl-next ]
jobs:
build:
@@ -24,4 +24,4 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run prettier
- run: npm run check-formatting
+586
View File
@@ -0,0 +1,586 @@
# 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))
## 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(...)` has to be
rewritten like this: `bus.addEventListener(...)`.
### 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
```
-1
View File
@@ -16,7 +16,6 @@ simple and consistent way. Owl's main features are:
- a declarative component system,
- a reactivity system based on hooks,
- concurrent mode by default,
- a store and a frontend router
Owl components are defined with ES6 classes, they use QWeb templates, an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
+1 -90
View File
@@ -189,95 +189,6 @@ with a `Proxy`, which means that it is totally transparent to the developers.
Adding new keys is supported. Once any part of the state has been changed, a
rendering is scheduled in the next microtask tick (promise queue).
## State Management
Managing the state of an application is a tricky issue. Many solutions have
been proposed these last few years. It also depends on the kind of application we
are talking about. A small application may not need much more than a simple
object to contain its state.
However, there are some common solutions for React and Vue: redux and vuex.
Both of them are a centralized store that own the state, and they dictate how
the state can be mutated.
**Redux**
In Redux, the state is mutated by reducers. Reducers are functions
that modify the state by returning a different object:
```javascript
...
switch (action.type) {
case ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false
}
}
};
}
```
This is a little bit awkward to write, but this allows the component system to
check if a part of the state was changed. This is exactly what is done by the
`connect` function: it create a _connected_ component, which is subscribed to
the state and triggers a rerender if some part of the state was modified.
**VueX**
VueX is based on a different principle: the state is mutated through
some special functions (the mutations), which modify the state in place:
```javascript
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:
each key from the state is silently replaced by getters and setters, and VueX
keeps track of who get data, and retrigger a render when it was changed.
**Owl**
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)).
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
},
};
const state = {
counter: { value: 0 },
};
const store = new owl.Store({ state, actions });
class Counter extends Component {
static template = xml`
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>`;
counter = useStore((state) => state.counter);
dispatch = useDispatch();
}
Counter.env.store = store;
const counter = new Counter();
```
## Hooks
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
@@ -342,4 +253,4 @@ class Example extends Component {
Since the Owl framework had hooks from early in its life, its main APIs
are designed to be interacted with hooks from the start. For example, the
`Context` and `Store` abstractions.
`Context` abstraction.
-2
View File
@@ -35,8 +35,6 @@ provided by Owl.
- [Props Validation](reference/props_validation.md)
- [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Slots](reference/slots.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
+1 -1
View File
@@ -263,7 +263,7 @@ We explain here all the public methods of the `Component` class.
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, or in the store, or ...) will be taken into account.
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
+2 -4
View File
@@ -158,12 +158,10 @@ There are two different common problems with Owl asynchronous rendering model:
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
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))
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
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
+14 -18
View File
@@ -11,30 +11,26 @@ browser
Component misc
Context AsyncRoot
QWeb Portal
mount router
Store Link
useState RouteComponent
config Router
mode
core tags
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
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
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
-2
View File
@@ -23,5 +23,3 @@ Its API is:
| `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions |
Note that the [`Store`](store.md) is an example of an `EventBus`.
-28
View File
@@ -18,9 +18,6 @@
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks)
@@ -375,31 +372,6 @@ to be closed:
useExternalListener(window, "click", this.closeMenu);
```
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
See the [store documentation](store.md) for more information.
### `useDispatch`
The `useDispatch` hook is the way for components to get a reference to the store
`dispatch` function. See the [store documentation](store.md) for more information.
### `useGetters`
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### `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
+2
View File
@@ -105,6 +105,8 @@ between Owl classes. This is the reason why `QWeb` actually extends [EventBus](e
### 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.
-181
View File
@@ -1,181 +0,0 @@
# 🦉 Router 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [RouteComponent](#routecomponent)
- [Link](#link)
## Overview
It is often useful to organize an application around urls. If the application is
a single page application, then we need a way to manage those urls in the browser.
This is why there are many different routers for different frameworks. A generic
router can do the job just fine, but a specialized router for Owl can give a
better developer experience.
The Owl router support the following features:
- `history` or `hash` mode
- declarative routes
- route redirection
- navigation guards
- parameterized routes
- a `<Link/>` component
- a `<RouteComponent/>` component
Note that it is still in early stage of developments, and there are probably
still some issues.
## Example
To use the Owl router, there are some steps that needs to be done:
- declare some routes
- create a router
- add it to the environment
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
export const ROUTES = [
{ name: "LANDING", path: "/", component: Landing },
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
{ name: "SIGN_UP", path: "/signup", component: SignUp },
{ name: "SIGN_IN", path: "/signin", component: SignIn },
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
];
function makeEnvironment() {
...
const env = { qweb };
env.session = new Session(env);
env.router = new owl.router.Router(env, ROUTES);
await env.router.start();
return env;
}
App.env = makeEnvironment();
// create root component here
```
Notice that the router needs to be started. This is an asynchronous operation
because it needs to apply the potential navigation guards on the current route
(which may or may not mean that the application is redirected to another route).
## Reference
### Route definition
A route need to be defined as an object with the following keys:
- `name` (optional): a (unique) string useful to identify the current route. If not
given, it will be assigned an automatic name,
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
It also can be `*`, to catch all remaining routes.
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
directive if the route is active
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
### `Router`
The `Router` constructor takes three arguments:
- `env`: a valid environment,
- a list of routes,
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
Example: `https://yourdomain.tld/my_custom_route`.\
For this mechanism to work, you need a way to configure your web server accordingly.
`hash` will manipulate the hash of the URL.\
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
```
Note that the route are defined in a list, and the order matters: the router
tries to find a match by going down the list.
The router needs to be added to the environment in the `router` sub key.
Once a router is created, it needs to be started. This is necessary to initialize
its current state to the current URL (and also, to potentially apply any
navigation guards and/or redirecting).
```js
await router.start();
```
Once started, the router will keep track of the current url and reflect its
value in two keys:
- `router.currentRoute`
- `router.currentParams`
The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url):
```js
router.navigate({ to: "USER", params: { id: 51 } });
```
### Navigation Guards
Navigation guards are very useful to be able to execute some business logic/
perform some actions or redirect to other routes whenever the application is
entering a new route. For example, the following guard checks if there is an
authenticated user, and if it is not the case, redirect to the sign in route.
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
```
A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object.
### `RouteComponent`
The `RouteComponent` component directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<RouteComponent />
</div>
```
### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any
content. It will compute the proper href from its props, and allow Owl to
properly navigate to a given url if clicked on it.
```xml
<Link to="'HOME'">Home</Link>
```
-349
View File
@@ -1,349 +0,0 @@
# 🦉 Store 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
Managing the state in an application is not an easy task. In some cases, the
state of an application can be part of the component tree, in a natural way.
However, there are situations where some parts of the state need to be displayed
in various parts of the user interface, and then, it is not obvious which
component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store to read their relevant
state, and they will be rerendered if the state is updated.
Note: Owl store is inspired by React Redux and VueX.
## Example
Here is what a simple store looks like:
```js
const actions = {
addTodo({ state }, message) {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false,
});
},
};
const state = {
todos: [],
nextId: 1,
};
const store = new owl.Store({ state, actions });
store.on("update", null, () => console.log(store.state));
// updating the state
store.dispatch("addTodo", "fix all bugs");
```
This example shows how a store can be defined and used. Note that in most cases,
actions will be dispatched by connected components.
## Reference
### `Store`
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
whenever its state is changed. Note that these events are triggered only after a
microtask tick, so only one event will be triggered for any number of state changes in a
call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
The `Store` class is quite small. It has two public methods:
- its constructor
- `dispatch`
The constructor takes a configuration object with four (optional) keys:
- the initial state
- the actions
- the getters
- the environment
```javascript
const config = {
state,
actions,
getters,
env,
};
const store = new Store(config);
```
### Actions
Actions are used to coordinate state changes. It can be used for both synchronous
and asynchronous logic.
```js
const actions = {
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo;
} catch (e) {
state.loginState = "error";
}
},
};
```
The first argument to an action method is an object with four keys:
- `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store,
- `env`: the current environment. This is useful sometimes, in particular if
an action needs to apply some side effects (such as performing an rpc), and
the `rpc` method is located in the environment.
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partially corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
},
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
},
};
```
### Getters
Usually, data contained in the store will be stored in a normalized way. For
example,
```js
{
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
authors: [{id: 4, name: 'John'}]
}
```
However, the user interface will probably need some denormalized data like
```js
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
```
This is what `getters` are for: they give a centralized way to process and
transform the data contained in the store.
```js
const getters = {
getPost({ state }, id) {
const post = state.posts.find((p) => p.id === id);
const author = state.authors.find((a) => a.id === post.id);
return {
id,
author,
content: post.content,
};
},
};
// somewhere else
const post = store.getters.getPost(id);
```
Getters take _at most_ one argument.
Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
means that the component needs a reference to the store. By default, it looks
for it in the `env.store` key. However, this can be configured with the `useStore`
hook.
Every component-store interactions are done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
Assume we have this store:
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
},
};
const state = {
counter: { value: 0 },
};
const store = new owl.Store({ state, actions });
```
To make it accessible to the complete application, we will put it in the
environment:
```js
// in this example, the root component is App
App.env.store = store;
```
A counter component can then select this value and dispatch an action like this:
```js
class Counter extends Component {
counter = useStore((state) => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>
```
### `useStore`
The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and which must return the part of the
store state that will be made available and observed for changes,
- optionally, an object which can have the following optional keys:
- a `store` key containing a store object if we want to use another store than
the default store,
- an `isEqual` key containing an equality function if we want to specialize
the comparison (the function must accept two arguments: the previous result
and the new result, and must return whether they are equal),
- and an `onUpdate` key containing an update function if we want to execute an
arbitrary code every time the selected state changes (the function will
receive one argument, the new result, and can execute arbitrary code).
If the `useStore` selector returns a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check (unless the `isEqual` option is defined,
then it will call it) and will update the component every time this check fails.
Note that if the selector function returns a primitive type, the result of
`useStore` will be immutable and it will not react to changes. In this case, it
is important to define the `onUpdate` option to properly update the value
manually when it changes.
Also, the return value from `useStore` is not supposed to be modified. The store
state should only be updated with actions.
### `useDispatch`
The `useDispatch` hook is useful when a component needs to be able to dispatch
actions. It takes an optional argument, which is a store. If not given, it will
use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class DoSomethingButton extends Component {
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
dispatch = useDispatch();
}
```
### `useGetters`
The `useGetters` hook is useful when a component needs to be able to use the
getters defined in a store. It takes an optional argument, which is a store. If
not given, it will use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class InfoButton extends Component {
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
getters = useGetters();
}
```
### Semantics
The `Store` class and the `useStore` hook try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children),
- they are updated only if they are in the DOM,
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components,
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and some other part of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately,
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `useStore`
call and a `Message` component could get the data of its own
message,
- since the `useStore` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+4 -4
View File
@@ -176,9 +176,9 @@ const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
[data-o-${uuid}] {
color: red;
}
`;
}
```
+6 -2
View File
@@ -16,6 +16,7 @@
"build:bundle": "rollup -c",
"build": "npm run build:bundle",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
"test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve",
@@ -48,14 +49,14 @@
"jest-environment-jsdom": "^27.1.0",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.4",
"prettier": "2.4.1",
"rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^27.0.5",
"typescript": "^3.7.2",
"typescript": "4.5.2",
"uglify-es": "^3.3.9"
},
"jest": {
@@ -64,6 +65,9 @@
"<rootDir>/src",
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
+1
View File
@@ -40,6 +40,7 @@ function getConfigForFormat(format, generatedFileName, minified = false) {
name: name,
extend: extend,
outro: outro,
freeze: false,
plugins: minified ? [terser()] : [],
indent: ' ', // indent with 4 spaces
};
+72
View File
@@ -0,0 +1,72 @@
import { Component } from "../component/component";
import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler";
import { TemplateSet } from "./template_set";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export interface Env {
[key: string]: any;
}
export interface AppConfig {
dev?: boolean;
env?: Env;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
}
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 = Object.freeze({});
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
root: ComponentNode | null = null;
constructor(Root: T, props?: any) {
super();
this.Root = Root;
this.props = props;
}
configure(config: AppConfig): App<T> {
if (config.dev) {
this.dev = config.dev;
console.info(DEV_MSG);
}
if (config.env) {
this.env = Object.freeze(Object.assign({}, config.env));
}
if (config.translateFn) {
this.translateFn = config.translateFn;
}
if (config.translatableAttributes) {
this.translatableAttributes = config.translatableAttributes;
}
return this;
}
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
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");
}
const node = new ComponentNode(this.Root, this.props, this);
this.root = node;
return node.mountComponent(target, options);
}
destroy() {
if (this.root) {
this.root.destroy();
}
}
}
+135
View File
@@ -0,0 +1,135 @@
import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
*/
function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callSlot(
ctx: any,
parent: any,
key: string,
name: string,
defaultSlot?: (ctx: any, key: string) => BDom,
dynamic?: boolean
): BDom {
const slots = ctx.__owl__.slots;
const slotFn = slots[name];
const slotBDom = slotFn ? slotFn(parent, key) : null;
if (defaultSlot) {
let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined;
if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else {
child2 = defaultSlot(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;
}
/*
* 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 (typeof value === "string") {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
return toggler(safeKey, block);
}
export const UTILS = {
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
capture,
withKey,
prepareList,
setContextValue,
shallowEqual,
toNumber,
validateProps,
safeOutput,
};
+65
View File
@@ -0,0 +1,65 @@
import { createBlock, html, list, multi, text, toggler } 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 };
export const globalTemplates: { [key: string]: string | Node } = {};
export class TemplateSet {
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
utils: typeof UTILS;
dev?: boolean;
constructor() {
const call = (subTemplate: string, ctx: any, parent: any) => {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template(ctx, parent));
};
const getTemplate = (name: string) => this.getTemplate(name);
this.utils = Object.assign({}, UTILS, { getTemplate, call });
}
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 } = {}) {
xml = xml instanceof Document ? xml : new DOMParser().parseFromString(xml, "text/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 = compile(rawTemplate, {
name,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
// 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];
}
}
+155
View File
@@ -0,0 +1,155 @@
import type { Setter } from "./block_compiler";
const { setAttribute, 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.
*/
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
return function (this: HTMLElement, value: any) {
if (value !== false) {
setAttribute.call(this, attr, value === true ? "" : 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): BlockType {
if (str in cache) {
return cache[str];
}
// step 0: prepare html base element
const doc = new DOMParser().parseFromString(`<t>${str}</t>`, "text/xml");
const node = doc.firstChild!.firstChild!;
if (config.shouldNormalizeDom) {
normalizeNode(node as any);
}
// step 1: prepare intermediate tree
const tree = buildTree(node);
// step 2: prepare block context
const context = buildContext(tree);
// step 3: build the final block class
const template = tree.el as HTMLElement;
const Block = buildBlock(template, context);
cache[str] = Block;
return Block;
}
// -----------------------------------------------------------------------------
// Helper
// -----------------------------------------------------------------------------
function normalizeNode(node: HTMLElement | Text) {
if (node.nodeType === Node.TEXT_NODE) {
if (!/\S/.test((node as Text).textContent!)) {
(node as Text).remove();
return;
}
}
if (node.nodeType === Node.ELEMENT_NODE) {
if ((node as HTMLElement).tagName === "pre") {
return;
}
}
for (let i = node.childNodes.length - 1; i >= 0; --i) {
normalizeNode(node.childNodes.item(i) as any);
}
}
// -----------------------------------------------------------------------------
// building a intermediate tree
// -----------------------------------------------------------------------------
interface DynamicInfo {
idx: number;
refIdx?: number;
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
isOnlyChild?: boolean;
name?: string;
tag?: string;
event?: string;
}
interface IntermediateTree {
parent: IntermediateTree | null;
firstChild: IntermediateTree | null;
nextSibling: IntermediateTree | null;
el: Node;
info: DynamicInfo[];
forceRef?: 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 isActive = false;
let currentNS = parent && parent.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("");
isActive = true;
}
if (tagName.startsWith("block-child-")) {
domParentTree!.forceRef = true;
const index = parseInt(tagName.slice(12), 10);
info.push({ type: "child", idx: index });
el = document.createTextNode("");
isActive = true;
}
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-")) {
isActive = true;
const idx = parseInt(attrName.slice(14), 10);
info.push({
type: "handler",
idx,
event: attrValue,
});
} else if (attrName.startsWith("block-attribute-")) {
isActive = true;
const idx = parseInt(attrName.slice(16), 10);
info.push({
type: "attribute",
idx,
name: attrValue,
tag: tagName,
});
} else if (attrName === "block-attributes") {
isActive = true;
info.push({
type: "attributes",
idx: parseInt(attrValue, 10),
});
} else if (attrName === "block-ref") {
isActive = true;
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: isActive ? 1 : 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 });
isActive = true;
tree.refN = 1;
} 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 (isActive) {
let cur: IntermediateTree | null = tree;
while ((cur = cur.parent)) {
cur.refN++;
}
}
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 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,
toIdx?: 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;
toIdx = tree.refN - 1;
}
if (tree.refN) {
const initialIdx = fromIdx!;
const isRef = tree.forceRef || tree.info.length > 0;
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, toIdx);
}
// left
if (firstChild) {
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
buildContext(tree.firstChild!, ctx, fromIdx!, toIdx! - nextSibling);
}
}
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): 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;
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() {
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 } 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;
}
+240
View File
@@ -0,0 +1,240 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeAppendChild = nodeProto.appendChild;
const nodeRemoveChild = nodeProto.removeChild;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
// -----------------------------------------------------------------------------
// List Node
// -----------------------------------------------------------------------------
class VList {
children: VNode[];
anchor: Node | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
constructor(children: VNode[]) {
this.children = children;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const _anchor = document.createTextNode("");
this.anchor = _anchor;
nodeInsertBefore.call(parent, _anchor, afterNode);
const l = children.length;
if (l) {
const mount = children[0].mount;
for (let i = 0; i < l; i++) {
mount.call(children[i], parent, _anchor);
}
}
this.parentEl = parent;
}
moveBefore(other: VList | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchor) || null;
}
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].moveBefore(null, afterNode);
}
this.parentEl!.insertBefore(this.anchor!, afterNode);
}
patch(other: VList, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const ch1 = this.children;
const ch2: VNode[] = other.children;
if (ch2.length === 0 && ch1.length === 0) {
return;
}
this.children = ch2;
const proto = ch2[0] || ch1[0];
const {
mount: cMount,
patch: cPatch,
remove: cRemove,
beforeRemove,
moveBefore: cMoveBefore,
firstNode: cFirstNode,
} = proto;
const _anchor = this.anchor!;
const isOnlyChild = this.isOnlyChild;
const parent = this.parentEl!;
// fast path: no new child => only remove
if (ch2.length === 0 && isOnlyChild) {
if (withBeforeRemove) {
for (let i = 0, l = ch1.length; i < l; i++) {
beforeRemove.call(ch1[i]);
}
}
nodeSetTextContent.call(parent, "");
nodeAppendChild.call(parent, _anchor);
return;
}
let startIdx1 = 0;
let startIdx2 = 0;
let startVn1 = ch1[0];
let startVn2 = ch2[0];
let endIdx1 = ch1.length - 1;
let endIdx2 = ch2.length - 1;
let endVn1 = ch1[endIdx1];
let endVn2 = ch2[endIdx2];
let mapping: any = undefined;
// let noFullRemove = this.hasNoComponent;
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
// -------------------------------------------------------------------
if (startVn1 === null) {
startVn1 = ch1[++startIdx1];
continue;
}
// -------------------------------------------------------------------
if (endVn1 === null) {
endVn1 = ch1[--endIdx1];
continue;
}
// -------------------------------------------------------------------
let startKey1 = startVn1.key;
let startKey2 = startVn2.key;
if (startKey1 === startKey2) {
cPatch.call(startVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = startVn1;
startVn1 = ch1[++startIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
let endKey1 = endVn1.key;
let endKey2 = endVn2.key;
if (endKey1 === endKey2) {
cPatch.call(endVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = endVn1;
endVn1 = ch1[--endIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (startKey1 === endKey2) {
// bnode moved right
cPatch.call(startVn1, endVn2, withBeforeRemove);
ch2[endIdx2] = startVn1;
const nextChild = ch2[endIdx2 + 1];
cMoveBefore.call(startVn1, nextChild, _anchor);
startVn1 = ch1[++startIdx1];
endVn2 = ch2[--endIdx2];
continue;
}
// -------------------------------------------------------------------
if (endKey1 === startKey2) {
// bnode moved left
cPatch.call(endVn1, startVn2, withBeforeRemove);
ch2[startIdx2] = endVn1;
const nextChild = ch1[startIdx1];
cMoveBefore.call(endVn1, nextChild, _anchor);
endVn1 = ch1[--endIdx1];
startVn2 = ch2[++startIdx2];
continue;
}
// -------------------------------------------------------------------
mapping = mapping || createMapping(ch1, startIdx1, endIdx1);
let idxInOld = mapping[startKey2];
if (idxInOld === undefined) {
cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);
} else {
const elmToMove = ch1[idxInOld];
cMoveBefore.call(elmToMove, startVn1, null);
cPatch.call(elmToMove, startVn2, withBeforeRemove);
ch2[startIdx2] = elmToMove;
ch1[idxInOld] = null as any;
}
startVn2 = ch2[++startIdx2];
}
// ---------------------------------------------------------------------
if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {
if (startIdx1 > endIdx1) {
const nextChild = ch2[endIdx2 + 1];
const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;
for (let i = startIdx2; i <= endIdx2; i++) {
cMount.call(ch2[i], parent, anchor);
}
} else {
for (let i = startIdx1; i <= endIdx1; i++) {
let ch = ch1[i];
if (ch) {
if (withBeforeRemove) {
beforeRemove.call(ch);
}
cRemove.call(ch);
}
}
}
}
}
beforeRemove() {
const children = this.children;
const l = children.length;
if (l) {
const beforeRemove = children[0].beforeRemove;
for (let i = 0; i < l; i++) {
beforeRemove.call(children[i]);
}
}
}
remove() {
const { parentEl, anchor } = this;
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const l = children.length;
if (l) {
const remove = children[0].remove;
for (let i = 0; i < l; i++) {
remove.call(children[i]);
}
}
nodeRemoveChild.call(parentEl, anchor!);
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : undefined;
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
}
}
export function list(children: VNode[]): VNode<VList> {
return new VList(children);
}
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
let mapping: any = {};
for (let i = startIdx1; i <= endIdx2; i++) {
mapping[ch1[i].key] = i;
}
return mapping;
}
+134
View File
@@ -0,0 +1,134 @@
import type { VNode } from "./index";
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
const nodeProto = Node.prototype;
const nodeInsertBefore = nodeProto.insertBefore;
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
const nodeRemoveChild = nodeProto.removeChild;
// -----------------------------------------------------------------------------
// Multi NODE
// -----------------------------------------------------------------------------
export class VMulti {
children: (VNode | undefined)[];
anchors?: Node[] | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
constructor(children: (VNode | undefined)[]) {
this.children = children;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const children = this.children;
const l = children.length;
const anchors = new Array(l);
for (let i = 0; i < l; i++) {
let child = children[i];
if (child) {
child.mount(parent, afterNode);
} else {
const childAnchor = document.createTextNode("");
anchors[i] = childAnchor;
nodeInsertBefore.call(parent, childAnchor, afterNode);
}
}
this.anchors = anchors;
this.parentEl = parent;
}
moveBefore(other: VMulti | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
}
const children = this.children;
const parent = this.parentEl;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
if (child) {
child.moveBefore(null, afterNode);
} else {
const anchor = anchors![i];
nodeInsertBefore.call(parent, anchor, afterNode);
}
}
}
patch(other: VMulti, withBeforeRemove: boolean) {
if (this === other) {
return;
}
const children1 = this.children;
const children2 = other.children;
const anchors = this.anchors!;
const parentEl = this.parentEl!;
for (let i = 0, l = children1.length; i < l; i++) {
const vn1 = children1[i];
const vn2 = children2[i];
if (vn1) {
if (vn2) {
vn1.patch(vn2, withBeforeRemove);
} else {
const afterNode = vn1.firstNode()!;
const anchor = document.createTextNode("");
anchors[i] = anchor;
nodeInsertBefore.call(parentEl, anchor, afterNode);
if (withBeforeRemove) {
vn1.beforeRemove();
}
vn1.remove();
children1[i] = undefined;
}
} else if (vn2) {
children1[i] = vn2;
const anchor = anchors[i];
vn2.mount(parentEl, anchor);
nodeRemoveChild.call(parentEl, anchor);
}
}
}
beforeRemove() {
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.beforeRemove();
}
}
}
remove() {
const parentEl = this.parentEl;
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
child.remove();
} else {
nodeRemoveChild.call(parentEl, anchors![i]);
}
}
}
}
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : this.anchors![0];
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
}
}
export function multi(children: (VNode | undefined)[]): VNode<VMulti> {
return new VMulti(children);
}
+70
View File
@@ -0,0 +1,70 @@
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;
class VText {
text: string;
parentEl?: HTMLElement | undefined;
el?: Text;
constructor(text: string) {
this.text = text;
}
mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent;
const node = document.createTextNode(toText(this.text));
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);
}
patch(other: VText) {
const text2 = other.text;
if (this.text !== text2) {
characterDataSetData.call(this.el!, toText(text2));
this.text = text2;
}
}
beforeRemove() {}
remove() {
nodeRemoveChild.call(this.parentEl, this.el!);
}
firstNode(): Node {
return this.el!;
}
toString() {
return this.text;
}
}
export function text(str: string): VNode<VText> {
return new VText(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);
}
-30
View File
@@ -1,30 +0,0 @@
export interface Browser {
setTimeout: Window["setTimeout"];
clearTimeout: Window["clearTimeout"];
setInterval: Window["setInterval"];
clearInterval: Window["clearInterval"];
requestAnimationFrame: Window["requestAnimationFrame"];
random: Math["random"];
Date: typeof Date;
fetch: Window["fetch"];
localStorage: Window["localStorage"];
}
let localStorage: Window["localStorage"] | null = null;
export const browser: Browser = {
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
setInterval: window.setInterval.bind(window),
clearInterval: window.clearInterval.bind(window),
requestAnimationFrame: window.requestAnimationFrame.bind(window),
random: Math.random,
Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window),
get localStorage() {
return localStorage || window.localStorage;
},
set localStorage(newLocalStorage: Window["localStorage"]) {
localStorage = newLocalStorage;
},
};
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
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;
}
let nextId = 1;
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");
const name = options.name || `template_${nextId++}`;
// code generation
const codeGenerator = new CodeGenerator(name, ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
// template function
return new Function("bdom, helpers", code) as TemplateFunction;
}
@@ -25,11 +25,12 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const 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 = Object.assign(Object.create(null), {
const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
and: "&&",
or: "||",
gt: ">",
@@ -69,6 +70,7 @@ interface Token {
size?: number;
varName?: string;
replace?: Function;
isLocal?: boolean;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
@@ -117,7 +119,7 @@ let tokenizeString: Tokenizer = function (expr) {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer) {
replace(replacer: any) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
@@ -223,8 +225,9 @@ export function tokenize(expr: string): Token[] {
// Expression "evaluator"
//------------------------------------------------------------------------------
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token) =>
const isLeftSeparator = (token: Token) =>
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token: Token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/**
@@ -252,10 +255,9 @@ const isRightSeparator = (token) =>
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
scope = Object.create(scope);
export function compileExprToArray(expr: string): Token[] {
const localVars = new Set<string>();
const tokens = tokenize(expr);
let i = 0;
let stack = []; // to track last opening [ or {
@@ -298,7 +300,7 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
}
}
if (token.type === "TEMPLATE_STRING") {
token.value = token.replace((expr) => compileExpr(expr, scope));
token.value = token.replace!((expr: any) => compileExpr(expr));
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
@@ -306,31 +308,49 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
}
} else {
scope[token.value] = { id: token.value, expr: token.value };
localVars.add(token.value); //] = { id: token.value, expr: token.value };
}
}
if (isVar) {
token.varName = token.value;
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
if (!localVars.has(token.value)) {
token.originalValue = token.value;
token.value = `scope['${token.value}']`;
token.value = `ctx['${token.value}']`;
}
}
i++;
}
// Mark all variables that have been used locally.
// This assumes the expression has only one scope (incorrect but "good enough for now")
for (const token of tokens) {
if (token.type === "SYMBOL" && localVars.has(token.value)) {
token.isLocal = true;
}
}
return tokens;
}
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope)
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 + "`";
}
+888
View File
@@ -0,0 +1,888 @@
// -----------------------------------------------------------------------------
// AST Type definition
// -----------------------------------------------------------------------------
export const enum ASTType {
Text,
Comment,
DomNode,
Multi,
TEsc,
TIf,
TSet,
TCall,
TOut,
TForEach,
TKey,
TComponent,
TDebug,
TLog,
TSlot,
TCallBlock,
TTranslation,
}
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[];
}
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;
}
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;
isOnlyChild: boolean;
hasNoComponent: 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]: AST };
}
export interface ASTSlot {
type: ASTType.TSlot;
name: 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 type AST =
| ASTText
| ASTComment
| ASTDomNode
| ASTMulti
| ASTTEsc
| ASTTif
| ASTTSet
| ASTTCall
| ASTTOut
| ASTTForEach
| ASTTKey
| ASTComponent
| ASTSlot
| ASTTCallBlock
| ASTLog
| ASTDebug
| ASTTranslation;
// -----------------------------------------------------------------------------
// 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!;
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) ||
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;
}
const children: AST[] = [];
for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
children.push(ast);
}
}
switch (children.length) {
case 0:
return null;
case 1:
return children[0];
default:
return {
type: ASTType.Multi,
content: children,
};
}
}
// -----------------------------------------------------------------------------
// 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;
}
const children: AST[] = [];
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");
for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
children.push(ast);
}
}
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;
}
}
if (children.length === 1 && children[0].type === ASTType.TForEach) {
children[0].isOnlyChild = true;
}
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,
key,
isOnlyChild: false,
hasNoComponent: hasNoComponent(body),
hasNoFirst,
hasNoLast,
hasNoIndex,
hasNoValue,
};
}
/**
* @returns true if we are sure the ast does not contain any component
*/
function hasNoComponent(ast: AST): boolean {
switch (ast.type) {
case ASTType.TComponent:
case ASTType.TOut:
case ASTType.TCall:
case ASTType.TCallBlock:
case ASTType.TSlot:
return false;
case ASTType.TSet:
case ASTType.Text:
case ASTType.Comment:
case ASTType.TEsc:
return true;
case ASTType.TKey:
return hasNoComponent(ast.content);
case ASTType.TDebug:
case ASTType.TLog:
case ASTType.TTranslation:
return ast.content ? hasNoComponent(ast.content) : true;
case ASTType.TForEach:
return ast.hasNoComponent;
case ASTType.Multi:
case ASTType.DomNode: {
for (let elem of ast.content) {
if (!hasNoComponent(elem)) {
return false;
}
}
return true;
}
case ASTType.TIf: {
if (!hasNoComponent(ast.content)) {
return false;
}
if (ast.tElif) {
for (let elem of ast.tElif) {
if (!hasNoComponent(elem.content)) {
return false;
}
}
}
if (ast.tElse && !hasNoComponent(ast.tElse)) {
return false;
}
return true;
}
}
}
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: tcall },
};
}
}
const body: AST[] = [];
for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
body.push(ast);
}
}
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);
if (!content) {
throw new Error("hmmm");
}
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();
}
return {
type: ASTType.TIf,
condition,
content,
tElif: tElifs.length ? tElifs : null,
tElse,
};
}
// -----------------------------------------------------------------------------
// 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 = [];
for (let child of node.childNodes) {
let childAst = parseNode(child, ctx);
if (childAst) {
body.push(childAst);
}
}
}
return { type: ASTType.TSet, name, value, defaultValue, body };
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
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-on-")) {
throw new Error(
"t-on is no longer supported on Component node. Consider passing a callback in props."
);
} 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) {
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) {
slots[name] = slotAst;
}
}
// default slot
const defaultContent = parseChildNodes(clone, ctx);
if (defaultContent) {
slots.default = 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;
}
return {
type: ASTType.TSlot,
name: node.getAttribute("t-slot")!,
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),
};
}
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
const children: AST[] = [];
for (let child of node.childNodes) {
const childAst = parseNode(child, ctx);
if (childAst) {
children.push(childAst);
}
}
switch (children.length) {
case 0:
return null;
case 1:
return children[0];
default:
return { type: ASTType.Multi, content: children };
}
}
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);
}
let tbranch = doc.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"
);
}
}
return doc;
}
+23 -770
View File
@@ -1,781 +1,34 @@
import { Observer } from "../core/observer";
import { OwlEvent } from "../core/owl_event";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
import type { Env } from "../app/app";
import type { ComponentNode } from "./component_node";
/**
* Owl Component System
*
* This file introduces a declarative and composable component system. It
* contains:
*
* - the Env interface (generic type for the environment)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
export type Props = { [key: string]: any };
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Component Class
// -----------------------------------------------------------------------------
/**
* 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;
browser: Browser;
}
export type MountPosition = "first-child" | "last-child" | "self";
interface MountOptions {
position?: MountPosition;
}
export const enum STATUS {
CREATED,
WILLSTARTED, // willstart has been called
RENDERED, // first render is completed (so, vnode is now defined)
MOUNTED, // is ready, and in DOM. It has a valid el
UNMOUNTED, // has a valid el, but is not in DOM
DESTROYED,
}
/**
* 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> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
depth: number;
vnode: VNode | null;
pvnode: VNode | null;
status: STATUS;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<any, T> | null;
children: { [key: number]: Component<any, T> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
currentFiber: Fiber | null;
// parentLastFiberId is there to help the parent component to detect, among
// its children, those that are not used anymore and thus can be destroyed
parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope'
// (typically when the component is rendered in a slot). We need to
// store that information in case the component would be re-rendered later on.
scope: any;
boundHandlers: { [key: number]: any };
observer: Observer | null;
renderFn: CompiledTemplate;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
willStartCB: Function | null;
willUpdatePropsCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
}
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<Props extends {} = any, T extends Env = Env> {
readonly __owl__: Internal<T>;
static template?: string | null = null;
static _template?: string | null = null;
static current: Component | null = null;
static components = {};
export class Component {
static template: string = "";
static style: string = "";
static props?: any;
static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
/**
* 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;
props: any;
env: Env;
__owl__: ComponentNode;
constructor(props: any, env: Env, node: ComponentNode) {
this.props = props;
this.env = env;
this.__owl__ = node;
}
get el(): HTMLElement | Text | undefined {
const node = this.__owl__;
return node.bdom ? (node.bdom.firstNode() as any) : undefined;
}
env: T;
props: Props;
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* 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<any, T> | null, props?: Props) {
Component.current = this;
let constr = this.constructor as any;
const defaultProps = constr.defaultProps;
if (defaultProps) {
props = props || ({} as Props);
this.__applyDefaultProps(props, defaultProps);
}
this.props = <Props>props;
if (QWeb.dev) {
QWeb.utils.validateProps(constr, this.props);
}
const id: number = nextId++;
let depth;
if (parent) {
this.env = parent.env;
const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
} else {
// we are the root component
this.env = (this.constructor as any).env;
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
// TODO: remove this in owl 2.0
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
switch (this.__owl__.status) {
case STATUS.MOUNTED:
this.render(true);
break;
case STATUS.DESTROYED:
// 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);
break;
}
});
depth = 0;
}
const qweb = this.env.qweb;
const template = constr.template || this.__getTemplate(qweb);
this.__owl__ = {
id: id,
depth: depth,
vnode: null,
pvnode: null,
status: STATUS.CREATED,
parent: parent || null,
children: {},
cmap: {},
currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
scope: null,
};
if (constr.style) {
this.__applyStyles(constr);
}
this.setup();
}
/**
* setup is run just after the component is constructed. This is the standard
* location where the component can setup its hooks. It has some advantages
* over the constructor:
* - it can be patched (useful in odoo ecosystem)
* - it does not need to propagate the arguments to the super call
*
* Note: this method should not be called manually.
*/
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() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* The willUpdateProps is an asynchronous hook, called just before new props
* are set. This is useful if the component needs some asynchronous task
* performed, depending on the props (for example, assuming that the props are
* some record Id, fetching the record data).
*
* This hook is not called during the first render (but willStart is called
* and performs a similar job).
*/
async willUpdateProps(nextProps: Props) {}
/**
* The willPatch hook is called just before the DOM patching process starts.
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*/
willPatch(): any {}
/**
* This hook is called whenever a component did actually update its props,
* state or env.
*
* This method is not called on the initial render. It is useful to interact
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*/
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
* unmounted from the DOM. This is a good place to remove some listeners, for
* example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*
* It needs to be implemented by a component that is designed to handle the
* error properly.
*/
catchError?(error?: Error): void;
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const position = options.position || "last-child";
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
switch (__owl__.status) {
case STATUS.CREATED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => {});
return scheduler.addFiber(fiber);
}
case STATUS.WILLSTARTED:
case STATUS.RENDERED:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case STATUS.UNMOUNTED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case STATUS.MOUNTED: {
if (position !== "self" && this.el!.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
} else {
return Promise.resolve();
}
}
case STATUS.DESTROYED:
throw new Error("Cannot mount a destroyed component");
}
}
/**
* 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__.status === STATUS.MOUNTED) {
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__;
const currentFiber = __owl__.currentFiber;
if (!__owl__.vnode && !currentFiber) {
return;
}
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const status = __owl__.status;
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) {
if (fiber.isCompleted || fiber.isRendered) {
return;
}
this.__render(fiber);
} else {
// we were mounted when render was called, but we aren't anymore, so we
// were actually about to be unmounted ; we can thus forget about this
// fiber
fiber.isCompleted = true;
__owl__.currentFiber = null;
}
});
return scheduler.addFiber(fiber);
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (__owl__.status !== STATUS.DESTROYED) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger<T = any>(eventType: string, payload?: T) {
this.__trigger<T>(this, eventType, payload);
}
//--------------------------------------------------------------------------
// 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 | null) {
const __owl__ = this.__owl__;
if (__owl__.status === STATUS.MOUNTED) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
}
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__.status = STATUS.DESTROYED;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
}
}
__callMounted() {
const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
}
__callWillUnmount() {
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
}
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.status === STATUS.MOUNTED) {
comp.__callWillUnmount();
}
}
}
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger<T>(component: Component, eventType: string, payload?: T) {
if (this.el) {
const ev = new OwlEvent<T>(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload,
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
triggerHook(ev);
}
this.el.dispatchEvent(ev);
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> {
this.__owl__.scope = scope;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
]);
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
this.__render(fiber);
}
}
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(target: HTMLElement | VNode | DocumentFragment, vnode: VNode) {
this.__owl__.vnode = patch(target as any, vnode);
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
this.__prepareAndRender(fiber, cb);
return fiber;
}
/**
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well. We then delete the
* `style` key from the constructor to make sure we do not apply it again.
*/
private __applyStyles(constr) {
while (constr && constr.style) {
if (constr.hasOwnProperty("style")) {
activateSheet(constr.style, constr.name);
delete constr.style;
}
constr = constr.__proto__;
}
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string = p.name;
while (!(template in qweb.templates) && p !== Component) {
p = p.__proto__;
template = p.name;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
return p._template;
}
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
const proms = Promise.all([
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = STATUS.WILLSTARTED;
await proms;
if (this.__owl__.status === <any>STATUS.DESTROYED) {
return Promise.resolve();
}
} catch (e) {
fiber.handleError(e);
return Promise.resolve();
}
if (!fiber.isCompleted) {
this.__render(fiber);
this.__owl__.status = STATUS.RENDERED;
cb();
}
}
__render(fiber: Fiber) {
const __owl__ = this.__owl__;
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let error;
try {
let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber,
});
// we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
}
}
if (!vnode) {
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
}
fiber.vnode = vnode;
// we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
const data = vnode.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
}
} catch (e) {
error = e;
}
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
fiber.root.counter--;
fiber.isRendered = true;
if (error) {
fiber.handleError(error);
}
}
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
__applyDefaultProps(props: Object, defaultProps: Object) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
render(deep: boolean = false): Promise<void> {
return this.__owl__.render(deep);
}
}
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
((C as any) as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
+329
View File
@@ -0,0 +1,329 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { clearReactivesForCallback, Reactive, reactive } from "../reactivity";
import { batched, Callback } from "../utils";
import { Component, Props } from "./component";
import { fibersInError, handleError } from "./error_handling";
import {
Fiber,
makeChildFiber,
makeRootFiber,
MountFiber,
MountOptions,
RootFiber,
__internal__destroyed
} from "./fibers";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
import { applyStyles } from "./style";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
if (!batchedRenderFunctions.has(currentNode!)) {
batchedRenderFunctions.set(
currentNode!,
batched(() => currentNode!.render())
);
}
const render = batchedRenderFunctions.get(currentNode!)!;
const reactiveState = reactive(state, render);
// manual implementation of onWillUnmount to break cyclic dependency
currentNode!.willUnmount.unshift( clearReactivesForCallback.bind(null, render))
return reactiveState;
}
// -----------------------------------------------------------------------------
// component function (used in compiled template code)
// -----------------------------------------------------------------------------
function arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return false;
}
export function component(
name: string | typeof Component,
props: any,
key: string,
ctx: ComponentNode,
parent: any,
hasSlots: boolean = false
): ComponentNode {
console.warn('asdf')
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) {
console.warn('coucou');
if (hasSlots || parentFiber.deep || arePropsDifferent(node.component.props, props)) {
console.warn('coucou3');
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 class
// -----------------------------------------------------------------------------
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);
slots: any = {};
refs: any = {};
willStart: LifecycleHook[] = [];
willUpdateProps: LifecycleHook[] = [];
willUnmount: LifecycleHook[] = [];
mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
destroyed: 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;
// if (props) {
// props = useState(props);
// }
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
if (C.style) {
applyStyles(C);
}
this.component.setup();
}
mountComponent(target: any, options?: MountOptions): Promise<InstanceType<T>> {
const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber);
return fiber.promise.then(() => this.component);
}
async initiateRender(fiber: Fiber | MountFiber) {
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(deep: boolean = false) {
let fiber = this.fiber;
if (fiber && !fiber.bdom && !fibersInError.has(fiber)) {
return fiber.root.promise;
}
if (!this.bdom && !fiber) {
// should find a way to return the future mounting promise
return;
}
fiber = makeRootFiber(this);
fiber.deep = deep;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
return;
}
if (this.fiber === fiber) {
this._render(fiber);
}
return fiber.root.promise;
}
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
fiber.root.counter--;
} catch (e) {
handleError({ node: this, error: e });
}
}
destroy() {
if (this.status === STATUS.MOUNTED) {
callWillUnmount(this);
this.bdom!.remove();
}
callDestroyed(this);
function callWillUnmount(node: ComponentNode) {
const component = node.component;
for (let cb of node.willUnmount) {
cb.call(component);
}
for (let child of Object.values(node.children)) {
if (child.status === STATUS.MOUNTED) {
callWillUnmount(child);
}
}
}
function callDestroyed(node: ComponentNode) {
const component = node.component;
node.status = STATUS.DESTROYED;
for (let child of Object.values(node.children)) {
callDestroyed(child);
}
for (let cb of node.destroyed) {
cb.call(component);
}
}
}
async updateAndRender(props: any, parentFiber: Fiber) {
// update
const fiber = makeChildFiber(this, parentFiber);
if (this.willPatch.length) {
parentFiber.root.willPatch.push(fiber);
}
if (this.patched.length) {
parentFiber.root.patched.push(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;
}
this.component.props = props;
this._render(fiber);
}
// ---------------------------------------------------------------------------
// 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() {
if (!this.fiber) {
// component was not rendered => no need to do anything
return;
}
this.bdom!.patch(this!.fiber!.bdom!, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
visitRemovedNodes(this);
}
remove() {
this.bdom!.remove();
}
}
function visitRemovedNodes(node: ComponentNode) {
if (node.status === STATUS.MOUNTED) {
const component = node.component;
for (let cb of node.willUnmount) {
cb.call(component);
}
}
for (let child of Object.values(node.children)) {
visitRemovedNodes(child);
}
node.status = STATUS.DESTROYED;
if (node.destroyed.length) {
__internal__destroyed.push(node);
}
}
-512
View File
@@ -1,512 +0,0 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
import { STATUS } from "./component";
//------------------------------------------------------------------------------
// 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;
},
});
}
};
QWeb.utils.assignHooks = function assignHooks(dataObj, hooks) {
if ("hook" in dataObj) {
const hookObject = dataObj.hook;
for (let name in hooks) {
const current = hookObject[name];
const fn = hooks[name];
if (current) {
hookObject[name] = (...args) => {
current(...args);
fn(...args);
};
} else {
hookObject[name] = fn;
}
}
} else {
dataObj.hook = hooks;
}
};
/**
* 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"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine(`// Component '${value}'`);
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events: [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-")) {
events.push([name, value]);
} else if (name === "t-transition") {
if (QWeb.enableTransitions) {
transition = value;
}
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map((k) => k + ":" + props[k])
.join(",");
let componentID = ctx.generateID();
let hasDefinedKey = false;
let templateKey;
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
defineComponentKey();
const id = ctx.generateID();
// the ___ is to make sure we have no possible conflict with normal
// template keys
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
templateKey = `k${id}`;
} else {
templateKey = ctx.generateTemplateKey();
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let finalizeComponentCode = `w${componentID}.destroy();`;
if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
delete w${componentID}.__owl__.transitionInserted;
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.toClassObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function ([name, value]) {
const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode(
ctx,
name,
value,
false,
T_COMPONENT_MODS_CODE
);
if (capture) {
return `vn.elm.addEventListener('${event}', ${handler}, true);`;
}
return `vn.elm.addEventListener('${event}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `utils.assignHooks(vnode.data, {create(_, vn){${styleCode}${eventsCode}}});`;
}
ctx.addLine(
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
);
let shouldProxy = !ctx.parentNode;
if (shouldProxy) {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
let registerCode = "";
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
// SLOTS
const hasSlots = node.childNodes.length;
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined";
ctx.addIf(`w${componentID}`);
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addElse();
// new component
function defineComponentKey() {
if (!hasDefinedKey) {
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
hasDefinedKey = true;
}
}
defineComponentKey();
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value);
ctx.addLine(
`let W${componentID} = ${contextualValue} || context.constructor.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} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
if (transition) {
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
ctx.addLine(
`w${componentID}.__patch = (t, vn) => {__patch${componentID}.call(w${componentID}, t, vn); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
);
}
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
// The next code is a fallback for compatibility reason. It accepts t-set
// elements that are direct children with a non empty body as nodes defining
// the content of a slot.
//
// This is wrong, but is necessary to prevent breaking all existing Owl
// code using slots. This will be removed in v2.0 someday. Meanwhile,
// please use t-set-slot everywhere you need to set the content of a
// slot.
for (let node of clone.children) {
if (node.hasAttribute("t-set") && node.hasChildNodes()) {
node.setAttribute("t-set-slot", node.getAttribute("t-set")!);
node.removeAttribute("t-set");
}
}
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
const slotNames = new Set<string>();
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];
// 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;
}
let key = slotNode.getAttribute("t-set-slot")!;
if (slotNames.has(key)) {
continue;
}
slotNames.add(key);
slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true });
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
let hasContent = false;
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
hasContent =
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
t.appendChild(child);
}
if (hasContent) {
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
}
ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true;
},
});
+57
View File
@@ -0,0 +1,57 @@
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) {
if (isFirstRound && fiber) {
fiber.root.counter--;
}
let propagate = true;
for (const h of errorHandlers) {
try {
h(error);
propagate = false;
} catch (e) {
error = e;
}
}
if (propagate) {
return _handleError(node.parent, error);
}
return true;
} else {
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!;
fibersInError.set(fiber.root, error);
const handled = _handleError(node, error, true);
if (!handled) {
try {
node.app.destroy();
} catch (e) {}
}
return handled;
}
-365
View File
@@ -1,365 +0,0 @@
import { h, VNode } from "../vdom/index";
import { Component, MountPosition, STATUS } from "./component";
import { scheduler } from "./scheduler";
/**
* Owl Fiber Class
*
* Fibers are small abstractions designed to contain all the internal state
* associated with a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*
* Fibers capture all that necessary information, which is critical to owl
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
* states and in general determine the state of the rendering.
*/
export class Fiber {
static nextId: number = 1;
id: number = Fiber.nextId++;
// The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCompleted means that the rendering corresponding to this fiber's work is
// done, either because the component has been mounted or patched, or because
// fiber has been cancelled.
isCompleted: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
// fibers corresponding to a new component do not need to do that. So, the
// shouldPatch hook is the boolean that we check whenever we need to apply
// a patch.
shouldPatch: boolean = true;
// isRendered is the last state of a fiber. If true, this means that it has
// been rendered and is inert (so, it should not be taken into account when
// counting the number of active fibers).
isRendered: boolean = false;
// the counter number is a critical information. It is only necessary for a
// root fiber. For that fiber, this number counts the number of active sub
// fibers. When that number reaches 0, the fiber can be applied by the
// scheduler.
counter: number = 0;
target: HTMLElement | DocumentFragment | null;
position: MountPosition | null;
scope: any;
component: Component;
vnode: VNode | null = null;
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
lastChild: Fiber | null = null;
parent: Fiber | null = null;
error?: Error;
constructor(
parent: Fiber | null,
component: Component,
force: boolean,
target: HTMLElement | DocumentFragment | null,
position: MountPosition | null
) {
this.component = component;
this.force = force;
this.target = target;
this.position = position;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
return oldFiber;
} else {
this._remapFiber(oldFiber);
}
}
this.root.counter++;
__owl__.currentFiber = this;
}
/**
* When the oldFiber is not completed yet, and both oldFiber and this fiber
* are root fibers, we want to reuse the oldFiber instead of creating a new
* one. Doing so will guarantee that the initiator(s) of those renderings will
* be notified (the promise will resolve) when the last rendering will be done.
*
* This function thus assumes that oldFiber is a root fiber.
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
oldFiber.lastChild = null;
}
oldFiber.counter = 1; // re-initialize counter
oldFiber.id = Fiber.nextId++;
}
/**
* In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component
* tree. In that case, it needs to cancel the previous rendering and
* remap itself as a part of the current parent rendering.
*/
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
this.shouldPatch = oldFiber.shouldPatch;
if (oldFiber === oldFiber.root) {
oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
this.parent = oldFiber.parent;
this.root = this.parent.root;
this.sibling = oldFiber.sibling;
if (this.parent.lastChild === oldFiber) {
this.parent.lastChild = this;
}
if (this.parent.child === oldFiber) {
this.parent.child = this;
} else {
let current = this.parent.child!;
while (true) {
if (current.sibling === oldFiber) {
current.sibling = this;
break;
}
current = current.sibling!;
}
}
}
}
/**
* This function has been taken from
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
*/
_walk(doWork: (f: Fiber) => Fiber | null) {
let root = this;
let current: Fiber = this;
while (true) {
const child = doWork(current);
if (child) {
current = child;
continue;
}
if (current === root) {
return;
}
while (!current.sibling) {
if (!current.parent || current.parent === root) {
return;
}
current = current.parent;
}
current = current.sibling;
}
}
/**
* Successfully complete the work of the fiber: call the mount or patch hooks
* and patch the DOM. This function is called once the fiber and its children
* are ready, and the scheduler decides to process it.
*/
complete() {
let component = this.component;
this.isCompleted = true;
const status = component.__owl__.status;
if (status === STATUS.DESTROYED) {
return;
}
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function (f) {
f.component.__owl__.currentFiber = null;
patchQueue.push(f);
return f.child;
};
this._walk(doWork);
const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue
if (status === STATUS.MOUNTED) {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
}
}
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.target && i === 0) {
let target;
if (fiber.position === "self") {
target = fiber.target;
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
throw new Error(
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
);
}
// In self mode, we *know* we are to take possession of the target
// Hence we manually create the corresponding VNode and copy the "key" in data
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
selfVnode.elm = target;
target = selfVnode;
} else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
component.__patch(target!, fiber.vnode!);
} else {
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
if (component.__owl__.pvnode) {
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
}
} else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.target) {
switch (this.position) {
case "first-child":
this.target.prepend(this.component.el!);
break;
case "last-child":
this.target.appendChild(this.component.el!);
break;
}
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
if (status === STATUS.MOUNTED || inDOM) {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else {
component.__callMounted();
}
}
} else {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = STATUS.UNMOUNTED;
}
}
}
/**
* Cancel a fiber and all its children.
*/
cancel() {
this._walk((f) => {
if (!f.isRendered) {
f.root.counter--;
}
f.isCompleted = true;
return f.child;
});
}
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
handleError(error: Error) {
let component = this.component;
this.vnode = component.__owl__.vnode || h("div");
const qweb = component.env.qweb;
let root = component;
function handle(error) {
let canCatch = false;
qweb.trigger("error", error);
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
if (canCatch) {
try {
component.catchError!(error);
} catch (e) {
root = component;
component = component.__owl__.parent!;
return handle(e);
}
return true;
}
return false;
}
let isHandled = handle(error);
if (!isHandled) {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
// at this point, the state of the application is corrupted and we could
// have a lot of issues or crashes. So we destroy the application in a try
// catch and swallow these errors because the fiber is already in error,
// and this is the actual issue that needs to be solved, not those followup
// errors.
try {
root.destroy();
} catch (e) {}
}
}
}
+215
View File
@@ -0,0 +1,215 @@
import type { BDom } from "../blockdom";
import { mount } from "../blockdom";
import type { ComponentNode } from "./component_node";
import { STATUS } from "./status";
import { fibersInError, handleError } from "./error_handling";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
// current is necessarily a rootfiber here
let root = parent.root;
cancelFibers(root, current.children);
current.children = [];
current.parent = parent;
root.counter++;
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);
}
return current;
}
const fiber = new RootFiber(node);
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;
deep: boolean = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
node.fiber = this;
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;
resolve: any;
promise: Promise<any>;
reject: any;
// only add stuff in this if they have registered some hooks
willPatch: Fiber[] = [];
patched: Fiber[] = [];
mounted: Fiber[] = [];
constructor(node: ComponentNode) {
super(node, null);
this.counter = 1;
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
complete() {
const node = this.node;
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);
this.appliedToDom = true;
// Step 3: calling all destroyed hooks
for (let node of __internal__destroyed) {
for (let cb of node.destroyed) {
cb();
}
}
__internal__destroyed.length = 0;
// 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();
}
}
}
// unregistering the fiber
node.fiber = null;
} catch (e) {
if (!handleError({ fiber: current || this, error: e })) {
this.reject(e);
}
}
}
}
export let __internal__destroyed: ComponentNode[] = [];
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);
this.target = target;
this.position = options.position || "last-child";
}
complete() {
let current: Fiber | undefined = this;
try {
const node = this.node;
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);
}
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();
}
}
}
node.fiber = null;
} catch (e) {
if (!handleError({ fiber: current as Fiber, error: e })) {
this.reject(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;
};
+72
View File
@@ -0,0 +1,72 @@
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 onDestroyed(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.destroyed.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;
};
}
export function onError(fn: (error: Error) => void | any) {
const node = getCurrent()!;
let handlers = nodeErrorHandlers.get(node);
if (handlers) {
handlers.push(fn);
} else {
handlers = [];
handlers.push(fn);
nodeErrorHandlers.set(node, handlers);
}
}
+34 -12
View File
@@ -1,4 +1,20 @@
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
@@ -10,24 +26,28 @@ import { QWeb } from "../qweb/index";
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
export 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) {
const propsDef = (<any>Widget).props;
applyDefaultProps(props, ComponentClass);
const propsDef = ComponentClass.props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
for (const propName of propsDef) {
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
throw new Error(`Unknown prop '${key}' given to component '${ComponentClass.name}'`);
}
}
} else if (propsDef) {
@@ -35,7 +55,7 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else {
continue;
}
@@ -44,16 +64,18 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
}
}
for (let propName in props) {
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}'`);
}
}
}
@@ -62,7 +84,7 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
/**
* 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) {
return true;
}
+38 -69
View File
@@ -1,24 +1,13 @@
import { Fiber } from "./fiber";
import { browser } from "../browser";
import { Fiber, RootFiber } from "./fibers";
import { fibersInError } from "./error_handling";
import { STATUS } from "./status";
/**
* Owl Scheduler Class
*
* The scheduler is the part of Owl that will effectively apply a rendering
* whenever a fiber is ready.
*
* Briefly, it can be used to register root fibers. Whenever there is an
* active root fiber, it will poll continuously each animation frame (so, about
* once every 16ms) and whenever a root fiber is ready, it will apply it.
*/
interface Task {
fiber: Fiber;
callback: (err?: Error) => void;
}
// -----------------------------------------------------------------------------
// Scheduler
// -----------------------------------------------------------------------------
export class Scheduler {
tasks: Task[] = [];
tasks: Set<RootFiber> = new Set();
isRunning: boolean = false;
requestAnimationFrame: Window["requestAnimationFrame"];
@@ -35,37 +24,10 @@ export class Scheduler {
this.isRunning = false;
}
addFiber(fiber: Fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
fiber = fiber.root;
return new Promise((resolve, reject) => {
if (fiber.error) {
return reject(fiber.error);
}
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
return reject(fiber.error);
}
resolve();
},
});
if (!this.isRunning) {
this.start();
}
});
}
rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root;
const index = this.tasks.findIndex((t) => t.fiber === fiber);
if (index >= 0) {
const [task] = this.tasks.splice(index, 1);
fiber.cancel();
fiber.error = new Error(reason);
task.callback();
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root);
if (!this.isRunning) {
this.start();
}
}
@@ -74,28 +36,37 @@ export class Scheduler {
* Other tasks are left unchanged.
*/
flush() {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter((task) => {
if (task.fiber.isCompleted) {
task.callback();
return false;
this.tasks.forEach((fiber) => {
if (fiber.root !== fiber) {
// this is wrong! should be something like
// if (this.tasks.has(fiber.root)) {
// // parent rendering has completed
// fiber.resolve();
// this.tasks.delete(fiber);
// }
this.tasks.delete(fiber);
return;
}
if (task.fiber.counter === 0) {
if (!task.fiber.error) {
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
const hasError = fibersInError.has(fiber);
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber);
fiber.reject(fibersInError.get(fiber));
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!hasError) {
fiber.complete();
fiber.resolve();
}
task.callback();
return false;
this.tasks.delete(fiber);
}
return true;
});
this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
if (this.tasks.size === 0) {
this.stop();
}
}
@@ -109,5 +80,3 @@ export class Scheduler {
});
}
}
export const scheduler = new Scheduler(browser.requestAnimationFrame);
+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";
}
}
@@ -1,13 +1,47 @@
import { Component } from "./component";
export const globalStylesheets: { [key: string]: HTMLStyleElement } = {};
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
globalStylesheets[id] = sheet;
}
/**
* Owl Style System
*
* This files contains the Owl code related to processing (extended) css strings
* and creating/adding <style> tags to the document head.
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well, in a reverse order to
* ensure that <style/> will be applied to the DOM in the order they are
* included in the document. We then delete the `style` key from the constructor
* to make sure we do not apply it again.
*/
export function applyStyles(ComponentClass: typeof Component) {
const toApply: [string, string][] = [];
while (ComponentClass && ComponentClass.style) {
if (ComponentClass.hasOwnProperty("style")) {
toApply.push([ComponentClass.style, ComponentClass.name]);
delete (ComponentClass as any).style;
}
ComponentClass = Object.getPrototypeOf(ComponentClass);
}
while (toApply.length) {
const [styleId, componentName] = toApply.pop()!;
activateSheet(styleId, componentName);
}
}
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
function activateSheet(id: string, name: string) {
const sheet = globalStylesheets[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.dataset.component = name;
document.head.appendChild(sheet);
}
export function processSheet(str: string): string {
function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
const selectorStack: string[][] = [];
const parts: string[] = [];
@@ -52,19 +86,3 @@ export function processSheet(str: string): string {
}
return parts.join("\n");
}
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
STYLESHEETS[id] = sheet;
}
export function activateSheet(id, name) {
const sheet = STYLESHEETS[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.setAttribute("component", name);
document.head.appendChild(sheet);
}
-42
View File
@@ -1,42 +0,0 @@
import { QWeb } from "./qweb/index";
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
/**
* This file creates and exports the OWL 'config' object, with keys:
* - 'mode': 'prod' or 'dev',
* - 'env': the environment to use in root components.
*/
interface Config {
mode: string;
enableTransitions: boolean;
translatableAttributes: string[];
}
export const config = {
translatableAttributes: TRANSLATABLE_ATTRS,
} as Config;
Object.defineProperty(config, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
console.info(`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.`);
}
},
});
Object.defineProperty(config, "enableTransitions", {
get() {
return QWeb.enableTransitions;
},
set(value: boolean) {
QWeb.enableTransitions = value;
},
});
-138
View File
@@ -1,138 +0,0 @@
import { Component } from "./component/component";
import { scheduler } from "./component/scheduler";
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
/**
* The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component,
* but when we have to deal with some mostly global information, this can be
* annoying, since each component will need to pass the information to each
* children, even though some or most of them will not use the information.
*
* With a `Context` object, each component can subscribe (with the `useContext`
* hook) to its state, and will be updated whenever the context state is updated.
*/
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
let lastGroup: T[] | false = false;
let lastValue;
return arr.reduce((acc: T[][], cur) => {
let curVal = fn(cur);
if (lastGroup) {
if (curVal === lastValue) {
lastGroup.push(cur);
} else {
lastGroup = false;
}
}
if (!lastGroup) {
lastGroup = [cur];
acc.push(lastGroup);
}
lastValue = curVal;
return acc;
}, []);
}
export class Context extends EventBus {
state: any;
observer: Observer;
rev: number = 1;
// mapping from component id to last observed context id
mapping: { [componentId: number]: number } = {};
constructor(state: Object = {}) {
super();
this.observer = new Observer();
this.observer.notifyCB = () => {
// notify components in the next microtask tick to ensure that subscribers
// are notified only once for all changes that occur in the same micro tick
let rev = this.rev;
return Promise.resolve().then(() => {
if (rev === this.rev) {
this.__notifyComponents();
}
});
};
this.state = this.observer.observe(state);
this.subscriptions.update = [];
}
/**
* 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. More precisely, if an update
* as an effect of destroying a children, we do not want to call any code
* from the child, and certainly not render it.
*
* This method implements a simple grouping algorithm by depth. If we have
* connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
* them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
* be updated sequentially, but each components in a given group will be done in
* parallel.
*
* This is a very simple algorithm, but it avoids checking if a given
* component is a child of another.
*/
async __notifyComponents() {
const rev = ++this.rev;
const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, (s) => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) {
const proms = group.map((sub) => sub.callback.call(sub.owner, rev));
// at this point, each component in the current group has registered a
// top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually
// flush the scheduler. This will force the scheduler to check
// immediately if they are done, which will cause their rendering
// promise to resolve earlier, which means that there is a chance of
// processing the next group in the same frame.
scheduler.flush();
await Promise.all(proms);
}
}
}
/**
* The`useContext` hook is the normal way for a component to register themselve
* to context state changes. The `useContext` method returns the context state
*/
export function useContext(ctx: Context): any {
const component: Component = Component.current!;
return useContextWithCB(ctx, component, component.render.bind(component));
}
export function useContextWithCB(ctx: Context, component: Component, method): any {
const __owl__ = component.__owl__;
const id = __owl__.id;
const mapping = ctx.mapping;
if (id in mapping) {
return ctx.state;
}
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
mapping[id] = 0;
const renderFn = __owl__.renderFn;
__owl__.renderFn = function (comp, params) {
mapping[id] = ctx.rev;
return renderFn(comp, params);
};
ctx.on("update", component, async (contextRev) => {
if (mapping[id] < contextRev) {
mapping[id] = contextRev;
await method();
}
});
const __destroy = component.__destroy;
component.__destroy = (parent) => {
ctx.off("update", component);
delete mapping[id];
__destroy.call(component, parent);
};
return ctx.state;
}
-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 = {};
}
}
-99
View File
@@ -1,99 +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;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
observe<T>(value: T, parent?: any): T {
if (
value === null ||
typeof value !== "object" ||
value instanceof Date ||
value instanceof Promise
) {
// 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;
}
_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.notifyCB();
}
return true;
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyCB();
}
return true;
},
});
const metadata = {
value,
proxy,
rev: 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);
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.rev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
-15
View File
@@ -1,15 +0,0 @@
import { Component } from "../component/component";
/**
* We define here OwlEvent, a subclass of CustomEvent, with an additional
* attribute:
* - originalComponent: the component that triggered the event
*/
export class OwlEvent<T> extends CustomEvent<T> {
originalComponent: Component;
constructor(component, eventType, options) {
super(eventType, options);
this.originalComponent = component;
}
}
+66 -127
View File
@@ -1,90 +1,6 @@
import { Component, Env } from "./component/component";
import { Observer } from "./core/observer";
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
// -----------------------------------------------------------------------------
// useState
// -----------------------------------------------------------------------------
/**
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component = Component.current!;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
// -----------------------------------------------------------------------------
// Life cycle hooks
// -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function () {
current.call(component);
cb.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
} else {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function () {
cb.call(component);
current.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
}
}
function makeAsyncHook(method: string) {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function (...args) {
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
};
} else {
component.__owl__[method] = cb;
}
};
}
export const onMounted = makeLifecycleHook("mountedCB", true);
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
export const onWillPatch = makeLifecycleHook("willPatchCB");
export const onPatched = makeLifecycleHook("patchedCB", true);
export const onWillStart = makeAsyncHook("willStartCB");
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
import type { Env } from "./app/app";
import { getCurrent } from "./component/component_node";
import { onMounted, onPatched, onWillPatch, onWillUnmount } from "./component/lifecycle_hooks";
// -----------------------------------------------------------------------------
// useRef
@@ -94,62 +10,85 @@ export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref<C extends Component = Component> {
el: HTMLElement | null;
comp: C | null;
}
export function useRef<C extends Component = Component>(name: string): Ref<C> {
const __owl__ = Component.current!.__owl__;
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
const node = getCurrent()!;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
if (val instanceof HTMLElement) {
return val;
} else if (val instanceof Component) {
return val.el;
}
return null;
},
get comp(): C | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? (val as C) : null;
get el(): T | null {
return node.refs[name] || null;
},
};
}
// -----------------------------------------------------------------------------
// "Builder" hooks
// useEnv and useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* 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 Component.current.env as any;
return getCurrent()!.component.env as any;
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
export function useSubEnv(envExtension: Env) {
const node = getCurrent()!;
node.childEnv = Object.freeze(Object.assign({}, node.childEnv, envExtension));
}
// -----------------------------------------------------------------------------
// 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;
});
let shouldReapplyOnPatch = false;
onWillPatch(() => {
const newDeps = computeDependencies();
shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapplyOnPatch) {
cleanup();
dependencies = newDeps;
}
});
onPatched(() => {
if (shouldReapplyOnPatch) {
cleanup = effect(...dependencies) || NO_OP;
}
});
onWillUnmount(() => cleanup());
}
// -----------------------------------------------------------------------------
@@ -172,11 +111,11 @@ export function useSubEnv(nextEnv) {
export function useExternalListener(
target: HTMLElement | typeof window,
eventName: string,
handler,
eventParams?
handler: EventListener,
eventParams?: AddEventListenerOptions
) {
const boundHandler = handler.bind(Component.current);
const node = getCurrent()!;
const boundHandler = handler.bind(node.component);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+72 -38
View File
@@ -1,42 +1,76 @@
/**
* This file is the main file packaged by rollup (see rollup.config.js). From
* this file, we export all public owl elements.
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index";
import { config } from "./config";
import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root";
import { Portal } from "./misc/portal";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router";
import {
config,
createBlock,
html,
list,
mount as blockMount,
multi,
patch,
remove,
text,
toggler,
} from "./blockdom";
import { mainEventHandler } from "./component/handler";
export { Component, mount } from "./component/component";
export { QWeb };
export { config };
export { browser } from "./browser";
config.shouldNormalizeDom = false;
config.mainEventHandler = mainEventHandler;
export const Context = _context.Context;
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const misc = { AsyncRoot, Portal };
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
useGetters: _store.useGetters,
useStore: _store.useStore,
});
export const blockDom = {
config,
// bdom entry points
mount: blockMount,
patch,
remove,
// bdom block types
list,
multi,
text,
toggler,
createBlock,
html,
};
import type { AppConfig } from "./app/app";
import { App } from "./app/app";
import { Component } from "./component/component";
import { getCurrent } from "./component/component_node";
export { useState } from "./component/component_node";
export { App, Component };
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement,
config: AppConfig = {}
): Promise<InstanceType<T>> {
const app = new App(C);
return app.configure(config).mount(target);
}
export function useComponent(): Component {
const current = getCurrent();
return current!.component;
}
export { status } from "./component/status";
export { Portal } from "./portal";
export { Memo } from "./memo";
export { css, xml } from "./tags";
export { 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,
onDestroyed,
onError,
} from "./component/lifecycle_hooks";
export const __info__ = {};
+47
View File
@@ -0,0 +1,47 @@
import { Component } from "./component/component";
import type { ComponentNode } from "./component/component_node";
import { xml } from "./tags";
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 (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
-19
View File
@@ -1,19 +0,0 @@
import { Component } from "../component/component";
import { xml } from "../tags";
/**
* AsyncRoot
*
* Owl is by default asynchronous, and the user interface will wait for all its
* subcomponents to be rendered before updating the DOM. This is most of the
* time what we want, but in some cases, it makes sense to "detach" a component
* from this coordination. This is the goal of the AsyncRoot component.
*/
export class AsyncRoot extends Component {
static template = xml`<t t-slot="default"/>`;
async __updateProps(nextProps, parentFiber) {
this.render(parentFiber.force);
}
}
-171
View File
@@ -1,171 +0,0 @@
import { Component, portalSymbol } from "../component/component";
import { VNode, patch } from "../vdom/index";
import { xml } from "../tags";
import { OwlEvent } from "../core/owl_event";
import { useSubEnv } from "../hooks";
/**
* Portal
*
* The Portal component allows to render a part of a component outside it's DOM.
* It is for example useful for dialogs: for css reasons, dialogs are in general
* placed in a specific spot of the DOM (e.g. directly in the body). With the
* Portal, a component can conditionally specify in its tempate that it contains
* a dialog, and where this dialog should be inserted in the DOM.
*
* The Portal component ensures that the communication between the content of
* the Portal and its parent properly works: business events reaching the Portal
* are re-triggered on an empty <portal> node located in the parent's DOM.
*/
interface Props {
target: string;
}
export class Portal extends Component<Props> {
static template = xml`<portal><t t-slot="default"/></portal>`;
static props = {
target: {
type: String,
},
};
// boolean to indicate whether or not we must listen to 'dom-appended' event
// to hook on the moment when the target is inserted into the DOM (because it
// is not when the portal is rendered)
doTargetLookUp: boolean = true;
// set of encountered events that need to be redirected
_handledEvents: Set<string> = new Set();
// function that will be the event's tunnel (needs to be an arrow function to
// avoid having to rebind `this`)
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
ev.stopPropagation();
this.__trigger(ev.originalComponent, ev.type, ev.detail);
};
// Storing the parent's env
parentEnv: any = null;
// represents the element that is moved somewhere else
portal: VNode | null = null;
// the target where we will move `portal`
target: Element | null = null;
constructor(parent, props) {
super(parent, props);
this.parentEnv = parent ? parent.env : {};
// put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them
useSubEnv({
[portalSymbol]: (ev) => {
if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type);
}
},
});
}
/**
* Override to revert back to a classic Component's structure
*
* @override
*/
__callWillUnmount() {
super.__callWillUnmount();
this.el!.appendChild(this.portal!.elm!);
this.doTargetLookUp = true;
}
/**
* At each DOM change, we must ensure that the portal contains exactly one
* child
*/
__checkVNodeStructure(vnode: VNode) {
const children = vnode.children!;
let countRealNodes = 0;
for (let child of children) {
if ((child as VNode).sel) {
countRealNodes++;
}
}
if (countRealNodes !== 1) {
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
}
}
/**
* Ensure the target is still there at whichever time we render
*/
__checkTargetPresence() {
if (!this.target || !document.contains(this.target)) {
throw new Error(`Could not find any match for "${this.props.target}"`);
}
}
/**
* Move the portal's element to the target
*/
__deployPortal() {
this.__checkTargetPresence();
this.target!.appendChild(this.portal!.elm!);
}
/**
* Override to remove from the DOM the element we have teleported
*
* @override
*/
__destroy(parent) {
if (this.portal && this.portal.elm) {
const displacedElm = this.portal.elm!;
const parent = displacedElm.parentNode;
if (parent) {
parent.removeChild(displacedElm);
}
}
super.__destroy(parent);
}
/**
* Override to patch the element that has been teleported
*
* @override
*/
__patch(target, vnode) {
if (this.doTargetLookUp) {
const target = document.querySelector(this.props.target);
if (!target) {
this.env.qweb.on("dom-appended", this, () => {
this.doTargetLookUp = false;
this.env.qweb.off("dom-appended", this);
this.target = document.querySelector(this.props.target);
this.__deployPortal();
});
} else {
this.doTargetLookUp = false;
this.target = target;
}
}
this.__checkVNodeStructure(vnode);
const shouldDeploy =
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
// this.target's parent has not been unmounted
this.__checkTargetPresence();
}
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
this.portal = patch(portalPatch, vnode.children![0] as VNode);
vnode.children = [];
super.__patch(target, vnode);
if (shouldDeploy) {
this.__deployPortal();
}
}
/**
* Override to set the env
*/
__trigger(component: Component, eventType: string, payload?: any) {
const env = this.env;
this.env = this.parentEnv;
super.__trigger(component, eventType, payload);
this.env = env;
}
}
+70
View File
@@ -0,0 +1,70 @@
import type { ComponentNode } from "./component/component_node";
import { Component } from "./component/component";
import { xml } from "./tags";
import { BDom, text, VNode } from "./blockdom";
const VText: any = text("").constructor;
class VPortal extends VText implements Partial<VNode<VPortal>> {
// selector: string;
realBDom: BDom | null;
target: HTMLElement | null = null;
constructor(selector: string, realBDom: BDom) {
super("");
this.selector = selector;
this.realBDom = realBDom;
}
mount(parent: HTMLElement, anchor: ChildNode) {
super.mount(parent, anchor);
this.target = document.querySelector(this.selector) as any;
if (!this.target) {
let el: any = this.el;
while (el && el.parentElement instanceof HTMLElement) {
el = el.parentElement;
}
this.target = el && el.querySelector(this.selector);
if (!this.target) {
throw new 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);
}
}
}
export class Portal extends Component {
static template = xml`<t t-slot="default"/>`;
static props = {
target: {
type: String,
},
};
constructor(props: any, env: any, node: ComponentNode) {
super(props, env, node);
node._render = function (fiber: any) {
const bdom = new VPortal(props.target, this.renderFn());
fiber.bdom = bdom;
fiber.root.counter--;
};
}
}
-395
View File
@@ -1,395 +0,0 @@
import { CompilationContext, INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/**
* 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.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
ctx.rootContext.shouldDefineScope = true;
if (value === "0") {
if (ctx.parentNode) {
// the 'zero' magical symbol is where we can find the result of the rendering
// of the body of the t-call.
ctx.rootContext.shouldDefineUtils = true;
const zeroArgs = ctx.escaping
? `{text: utils.vDomToString(scope[utils.zero])}`
: `...scope[utils.zero]`;
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} != null`);
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
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(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else {
ctx.rootContext.shouldDefineUtils = true;
if (value.hasBody) {
ctx.addLine(
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
);
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
ctx.closeIf();
}
QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
},
});
QWeb.addDirective({
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }): boolean {
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
},
});
//------------------------------------------------------------------------------
// t-set
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
let scopeExpr = `scope`;
if (ctx.protectedScopeNumber) {
ctx.rootContext.shouldDefineUtils = true;
scopeExpr = `utils.getScope(scope, '${variable}')`;
}
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
if (hasBody) {
ctx.rootContext.shouldDefineUtils = true;
if (value) {
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr);
}
qweb._compileNode(nodeCopy, ctx);
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
qwebvar.value = `c${tempParentNodeID}`;
qwebvar.hasBody = true;
ctx.parentNode = _parentNode;
if (value) {
ctx.closeIf();
}
}
return true;
},
});
//------------------------------------------------------------------------------
// t-if, t-elif, t-else
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "if",
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
return false;
},
finalize({ ctx }) {
ctx.closeIf();
},
});
QWeb.addDirective({
name: "elif",
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
},
});
QWeb.addDirective({
name: "else",
priority: 40,
atNodeEncounter({ ctx }): boolean {
ctx.addLine(`else {`);
ctx.indent();
return false;
},
finalize({ ctx }) {
ctx.closeIf();
},
});
//------------------------------------------------------------------------------
// t-call
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
const subTemplate = node.getAttribute("t-call")!;
const isDynamic = INTERP_REGEXP.test(subTemplate);
const nodeTemplate = qweb.templates[subTemplate];
if (!isDynamic && !nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
// Step 2: compile target template in sub templates
// ------------------------------------------------
let subIdstr: string;
if (isDynamic) {
const _id = ctx.generateID();
ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`);
ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`);
ctx.addIf(`!tid${_id}`);
ctx.addLine(`tid${_id} = this.constructor.nextId++;`);
ctx.addLine(`this.subTemplates[tname${_id}] = tid${_id};`);
ctx.addLine(
`this.constructor.subTemplates[tid${_id}] = this._compile(tname${_id}, {hasParent: true, defineKey: true});`
);
ctx.closeIf();
subIdstr = `tid${_id}`;
} else {
let subId = qweb.subTemplates[subTemplate];
if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
const subTemplateFn = qweb._compile(subTemplate, { hasParent: true, defineKey: true });
QWeb.subTemplates[subId] = subTemplateFn;
}
subIdstr = `'${subId}'`;
}
// Step 3: compile t-call body if necessary
// ------------------------------------------------
let hasBody = node.hasChildNodes();
const protectID = ctx.startProtectScope();
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent();
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr);
}
// this local scope is intended to trap c__0
ctx.addLine(`{`);
ctx.indent();
ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0"));
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;");
ctx.dedent();
ctx.addLine(`}`);
}
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const parentComponent = ctx.rootContext.shouldDefineParent
? `parent`
: `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.dedent();
ctx.addLine(`}`);
}
ctx.stopProtectScope(protectID);
return true;
},
});
//------------------------------------------------------------------------------
// t-foreach
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "foreach",
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`let _${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(`let _${keysID} = _${arrayID};`);
ctx.addLine(`let _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope(true);
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
ctx.addLine(`scope.${name}_index = ${loopVar}`);
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key");
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
if (nodeCopy.hasAttribute("t-key")) {
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
nodeCopy.removeAttribute("t-key");
} else {
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
ctx.stopProtectScope(varsID);
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})`);
},
});
-211
View File
@@ -1,211 +0,0 @@
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class CompilationContext {
static nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null | string = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: CompilationContext;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
protectedScopeNumber: number = 0;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
hasKey0: boolean = false;
keyStack: boolean[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("let h = this.h;");
}
generateID(): number {
return CompilationContext.nextID++;
}
/**
* This method generates a "template key", which is basically a unique key
* which depends on the currently set keys, and on the iteration numbers (if
* we are in a loop).
*
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(prefix: string = ""): string {
const id = this.generateID();
if (this.loopNumber === 0 && !this.hasKey0) {
return `'${prefix}__${id}__'`;
}
let key = `\`${prefix}__${id}__`;
let start = this.hasKey0 ? 0 : 1;
for (let i = start; i < this.loopNumber + 1; i++) {
key += `\${key${i}}__`;
}
this.addLine(`let k${id} = ${key}\`;`);
return `k${id}`;
}
generateCode(): string[] {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
}
withParent(node: number): CompilationContext {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
if (!this.parentNode && this.rootContext.shouldDefineResult) {
this.addLine(`result = vn${node};`);
}
return this.subContext("parentNode", node);
}
subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.rootContext.indentLevel++;
}
dedent() {
this.rootContext.indentLevel--;
}
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
return this.code.length - 1;
}
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): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
captureExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
const argId = this.generateID();
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map((tok) => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
}
tok.value = `${tok.varName}_${argId}`;
}
return tok.value;
})
.join("");
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(codeBlock?: boolean): number {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
const scopeExpr = `Object.create(scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
if (!codeBlock) {
this.addLine(`scope.__access_mode__ = 'ro';`);
}
return protectID;
}
stopProtectScope(protectID: number) {
this.rootContext.protectedScopeNumber--;
this.addLine(`scope = _origScope${protectID};`);
}
}
-377
View File
@@ -1,377 +0,0 @@
import { STATUS } from "../component/component";
import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
import { browser } from "../browser";
/**
* 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();",
};
interface HandlerInfo {
event: string;
handler: string;
}
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
export function makeHandlerCode(
ctx,
fullName,
value,
putInCache: boolean,
modcodes = MODS_CODE
): HandlerInfo {
let [event, ...mods] = fullName.slice(5).split(".");
if (mods.includes("capture")) {
event = "!" + event;
}
if (!event) {
throw new Error("Missing event name with t-on directive");
}
let code: string;
// check if it is a method with no args, a method with args or an expression
let args: string = "";
const name: string = value.replace(/\(.*\)/, function (_args) {
args = _args.slice(1, -1);
return "";
});
const isMethodCall = name.match(FNAMEREGEXP);
// then generate code
if (isMethodCall) {
ctx.rootContext.shouldDefineUtils = true;
const comp = `utils.getComponent(context)`;
if (args) {
const argId = ctx.generateID();
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
code = `${comp}['${name}'](...args${argId}, e);`;
putInCache = false;
} else {
code = `${comp}['${name}'](e);`;
}
} else {
// if we get here, then it is an expression
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(value);
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
}
const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
handler = `extra.handlers[${key}]`;
}
return { event, handler };
}
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
},
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
},
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function (cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
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 = () => {
if (!elm.classList.contains(name + "-leave-active")) {
return;
}
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) {
if (!elm.parentNode) {
// if we get here, this means that the element was removed for some other
// reasons, and in that case, we don't want to work on animation since nothing
// will be displayed anyway.
return;
}
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) {
const transitionEndCB = () => {
if (!elm.parentNode) return;
cb();
browser.clearTimeout(fallbackTimeout);
elm.removeEventListener("transitionend", transitionEndCB);
};
elm.addEventListener("transitionend", transitionEndCB, { once: true });
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
if (!QWeb.enableTransitions) {
return;
}
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`,
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
},
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
if (!ctx.parentNode) {
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.generateID()}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
if (node.hasChildNodes()) {
ctx.addElse();
const nodeCopy = <Element>node.cloneNode(true);
nodeCopy.removeAttribute("t-slot");
qweb._compileNode(nodeCopy, ctx);
}
ctx.closeIf();
return true;
},
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function (val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
// First step: we need to understand the structure of the expression, and
// from it, extract a base expression (that we can capture, which is
// important because it will be used in a handler later) and a formatted
// expression (which uses the captured base expression)
//
// Also, we support 2 kinds of values: some.expr.value or some.expr[value]
// For the first one, we have:
// - base expression = scope[some].expr
// - expression = exprX.value (where exprX is the var that captures the base expr)
// and for the expression with brackets:
// - base expression = scope[some].expr
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
// and keyX captures scope[value])
let expr: string;
let baseExpr: string;
if (hasDotAtTheEnd.test(value)) {
// we manage the case where the expr has a dot: some.expr.value
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
expr = `expr${nodeID}${value.slice(index)}`;
} else if (hasBracketsAtTheEnd.test(value)) {
// we manage here the case where the expr ends in a bracket expression:
// some.expr[value]
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
let exprKey = value.trimRight().slice(index + 1, -1);
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
expr = `expr${nodeID}[exprKey${nodeID}]`;
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
const key = ctx.generateTemplateKey();
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
event = "change";
handler = `(ev) => {${expr} = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
handler = `(ev) => {${expr} = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
handler = `(ev) => {${expr} = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
},
});
//------------------------------------------------------------------------------
// t-key
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value, node }) {
if (ctx.loopNumber === 0) {
ctx.keyStack.push(ctx.rootContext.hasKey0);
ctx.rootContext.hasKey0 = true;
}
ctx.addLine("{");
ctx.indent();
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
},
finalize({ ctx }) {
ctx.dedent();
ctx.addLine("}");
if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
}
},
});
-4
View File
@@ -1,4 +0,0 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
-930
View File
@@ -1,930 +0,0 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual, escape } from "../utils";
import { addNS } from "../vdom/vdom";
/**
* Owl QWeb Engine
*
* In this file, you will find a QWeb engine/template compiler. It is the core
* of how Owl component works.
*
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
* DOM representation.
*
* We have here:
* - a CompilationContext class, which is an internal object that contains all
* compilation specific information, while a template is being compiled.
* - a QWeb class: this is the code of the QWeb compiler.
*
* Note that this file does not contain the implementation of the QWeb
* directives (see qweb_directives.ts and qweb_extensions.ts).
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type EvalContext = { [key: string]: any };
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
interface Template {
elem: Element;
fn: CompiledTemplate;
}
interface CompilationInfo {
node: Element;
qweb: QWeb;
ctx: CompilationContext;
fullName: string;
value: string;
}
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive {
name: string;
extraNames?: string[];
priority: number;
// if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void;
}
interface QWebConfig {
templates?: string;
translateFn?(text: string): string;
}
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)",
destroy: "()",
};
interface Utils {
toClassObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__");
}
class VDomArray extends Array {
toString() {
return vDomToString(this);
}
}
function vDomToString(vdom: VNode[]): string {
return vdom
.map((vnode) => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
}
const UTILS: Utils = {
zero: Symbol("zero"),
toClassObj(expr) {
const result = {};
if (typeof expr === "string") {
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
// this is already an object, but we may need to split keys:
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
for (let key in expr) {
const value = expr[key];
const words = key.split(/\s+/);
for (let word of words) {
result[word] = result[word] || value;
}
}
return result;
},
/**
* This method combines the current context with the variables defined in a
* scope for use in a slot.
*
* The implementation is kind of tricky because we want to preserve the
* prototype chain structure of the cloned result. So we need to traverse the
* prototype chain, cloning each level respectively.
*/
combine(context, scope) {
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
},
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
},
VDomArray,
vDomToString,
getComponent(obj) {
while (obj && !isComponent(obj)) {
obj = obj.__proto__;
}
return obj;
},
getScope(obj, property: string) {
const obj0 = obj;
while (
obj &&
!obj.hasOwnProperty(property) &&
!(obj.hasOwnProperty("__access_mode__") && obj.__access_mode__ === "ro")
) {
const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) {
return obj0;
}
obj = newObj;
}
return obj;
},
};
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;
}
function escapeQuotes(str: string): string {
return str.replace(/\'/g, "\\'");
}
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template };
static utils = UTILS;
static components = Object.create(null);
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
name: 1,
att: 1,
attf: 1,
translation: 1,
tag: 1,
};
static DIRECTIVES: Directive[] = [];
static TEMPLATES: { [name: string]: Template } = {};
static nextId: number = 1;
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
static enableTransitions: boolean = true;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
static slots = {};
static nextSlotId = 1;
// subTemplates are stored in two objects: a (local) mapping from a name to an
// id, and a (global) mapping from an id to the compiled function. This is
// necessary to ensure that global templates can be called with more than one
// QWeb instance.
subTemplates: { [key: string]: number } = {};
static subTemplates: { [id: number]: Function } = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
constructor(config: QWebConfig = {}) {
super();
this.templates = Object.create(QWeb.TEMPLATES);
if (config.templates) {
this.addTemplates(config.templates);
}
if (config.translateFn) {
this.translateFn = config.translateFn;
}
}
static addDirective(directive: Directive) {
if (directive.name in QWeb.DIRECTIVE_NAMES) {
throw new Error(`Directive "${directive.name} already registered`);
}
QWeb.DIRECTIVES.push(directive);
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
static registerComponent(name: string, Component: any) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.components[name] = Component;
}
/**
* Register globally a template. All QWeb instances will obtain their
* templates from their own template map, and then, from the global static
* TEMPLATES property.
*/
static registerTemplate(name: string, template: string) {
if (QWeb.TEMPLATES[name]) {
throw new Error(`Template '${name}' has already been registered`);
}
const qweb = new QWeb();
qweb.addTemplate(name, template);
QWeb.TEMPLATES[name] = qweb.templates[name];
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
}
this._addTemplate(name, <Element>doc.firstChild);
}
/**
* Load templates from a xml (as a string or xml document). This will look up
* for the first <templates> tag, and will consider each child of this as a
* template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr: string | Document) {
if (!xmlstr) {
return;
}
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
}
for (let elem of <any>templates.children) {
const name = elem.getAttribute("t-name");
this._addTemplate(name, elem);
}
}
_addTemplate(name: string, elem: Element) {
if (name in this.templates) {
throw new Error(`Template ${name} already defined`);
}
this._processTemplate(elem);
const template = {
elem,
fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name);
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 (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"
);
}
}
}
/**
* Render a template
*
* @param {string} name the template should already have been added
*/
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
const template = this.templates[name];
if (!template) {
throw new Error(`Template ${name} does not exist`);
}
return template.fn.call(this, context, extra);
}
/**
* Render a template to a html string.
*
* Note that this is more limited than the `render` method: it is not suitable
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const elem = patch(node, vnode).elm as HTMLElement;
function escapeTextNodes(node) {
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.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,
options: {
elem?: Element;
hasParent?: boolean;
defineKey?: boolean;
} = {}
): CompiledTemplate {
const elem = options.elem || this.templates[name].elem;
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (options.hasParent) {
ctx.variables = Object.create(null);
ctx.parentNode = ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.shouldDefineParent = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (options.defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
}
this._compileNode(elem, ctx);
if (!options.hasParent) {
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();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template;
try {
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: CompilationContext) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
const match = translationRE.exec(text);
text = match[1] + this.translateFn(match[2]) + match[3];
}
}
if (ctx.parentNode) {
if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (node.nodeType === 8) {
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
}
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
throw new Error(
`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`
);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
const finalizers: typeof validDirectives = [];
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child);
}
node.appendChild(tNode);
node.removeAttribute(attrName);
}
}
}
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
let withHandlers = false;
for (let i = 0; i < DIR_N; i++) {
let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
finalizers.push({ directive, value, fullName });
}
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value,
});
if (isDone) {
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
return;
}
}
}
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
let nodeHooks = {};
let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
const value = node.getAttribute("t-att-value");
let exprId = ctx.generateID();
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
let expr = `expr${exprId}`;
node.setAttribute("t-att-value", expr);
addNodeHook("create", `n.elm.value=${expr};`);
}
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
directive.atNodeCreation({
node,
qweb: this,
ctx,
fullName,
value,
nodeID,
addNodeHook,
});
}
}
if (Object.keys(nodeHooks).length) {
ctx.addLine(`p${nodeID}.hook = {`);
for (let hook in nodeHooks) {
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
for (let handler of nodeHooks[hook]) {
ctx.addLine(` ${handler}`);
}
ctx.addLine(` },`);
}
ctx.addLine(`};`);
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS =
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
}
_compileGenericNode(
node: ChildNode,
ctx: CompilationContext,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleProperties(key, val) {
let isProp = false;
switch (node.nodeName) {
case "input":
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
if (key === "checked" || key === "indeterminate") {
isProp = true;
}
}
if (key === "value" || key === "readonly" || key === "disabled") {
isProp = true;
}
break;
case "option":
isProp = key === "selected" || key === "disabled";
break;
case "textarea":
isProp = key === "readonly" || key === "disabled" || key === "value";
break;
case "select":
isProp = key === "disabled" || key === "value";
break;
case "button":
case "optgroup":
isProp = key === "disabled";
break;
}
if (isProp) {
props.push(`${key}: ${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
let value = attributes[i].textContent!;
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
value = this.translateFn(value);
}
// regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map((a) => `'${escapeQuotes(a)}':true`)
.join(",");
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
}
} else {
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleProperties(name, `_${attID}`);
}
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toClassObj(${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(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
if (node.nodeName === "select" && attName === "value") {
attrs.push(`${attName}: ${v}`);
handleProperties(attName, v);
} else {
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(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(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`let _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
const parts = [`key:${key}`];
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();
}
let nodeName = `'${node.nodeName}'`;
if ((<Element>node).hasAttribute("t-tag")) {
const tagExpr = (<Element>node).getAttribute("t-tag");
(<Element>node).removeAttribute("t-tag");
nodeName = `tag${ctx.generateID()}`;
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
}
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = vn${nodeID};`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: CompilationContext) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
+190
View File
@@ -0,0 +1,190 @@
import { Callback } from "./utils";
// 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;
export 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);
}
/**
* 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
*/
export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
callbacks.delete(callback);
}
}
}
const reactiveCache = new WeakMap<Target, Map<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);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
-48
View File
@@ -1,48 +0,0 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./router";
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Props, Env> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>
`;
href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) {
this.href = this.env.router.destToPath(nextProps);
}
get isActive() {
if (this.env.router.mode === "hash") {
return (<any>document.location).hash === this.href;
}
return (<any>document.location).pathname === this.href;
}
navigate(ev) {
// don't redirect with control keys
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
// don't redirect on right click
if (ev.button !== undefined && ev.button !== 0) {
return;
}
// don't redirect if `target="_blank"`
if (ev.currentTarget && ev.currentTarget.getAttribute) {
const target = ev.currentTarget.getAttribute("target");
if (/\b_blank\b/i.test(target)) {
return;
}
}
ev.preventDefault();
this.env.router.navigate(this.props);
}
}
-19
View File
@@ -1,19 +0,0 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { EnvWithRouter } from "./router";
export class RouteComponent extends Component<{}, EnvWithRouter> {
static template = xml`
<t>
<t
t-if="routeComponent"
t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t>
`;
get routeComponent(): any {
return this.env.router.currentRoute && this.env.router.currentRoute.component;
}
}
-294
View File
@@ -1,294 +0,0 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { shallowEqual } from "../utils";
type NavigationGuard = (info: {
env: Env;
to: Route | null;
from: Route | null;
}) => boolean | Destination;
export interface Route {
name: string;
path: string;
extractionRegExp: RegExp;
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"];
}
export interface EnvWithRouter extends Env {
router: Router;
}
const paramRegexp = /\{\{(.*?)\}\}/;
const globalParamRegexp = new RegExp(paramRegexp.source, "g");
export class Router {
currentRoute: Route | null = null;
currentParams: RouteParams | null = null;
mode: "history" | "hash";
routes: { [id: string]: Route };
routeIds: string[];
env: RouterEnv;
constructor(
env: Partial<EnvWithRouter>,
routes: Partial<Route>[],
options: Options = { mode: "history" }
) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
this.routes = {};
this.routeIds = [];
let nextId = 1;
for (let partialRoute of routes) {
if (!partialRoute.name) {
partialRoute.name = "__route__" + nextId++;
}
if (partialRoute.component) {
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
}
if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
partialRoute.extractionRegExp = makeExtractionRegExp(partialRoute.path);
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
(this as any)._listener = (ev) => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener);
}
const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") {
this.currentRoute = result.route;
this.currentParams = result.params;
const currentPath = this.routeToPath(result.route, result.params);
if (currentPath !== this.currentPath()) {
this.setUrlFromPath(currentPath);
}
}
}
async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to);
return this._navigate(path);
}
async _navigate(path: string, ev?: any): Promise<boolean> {
const initialName = this.currentRouteName;
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
let finalPath = this.routeToPath(result.route, result.params);
if (path.indexOf("?") > -1) {
finalPath += "?" + path.split("?")[1];
}
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath);
}
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
const didChange =
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
if (didChange) {
this.env.qweb.forceUpdate();
return true;
}
return false;
}
destToPath(dest: Destination): string {
this.validateDestination(dest);
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
}
get currentRouteName(): string | null {
return this.currentRoute && this.currentRoute.name;
}
//--------------------------------------------------------------------------
// Private helpers
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const separator = this.mode === "hash" ? location.pathname : "";
const url = location.origin + separator + path;
if (url !== window.location.href) {
window.history.pushState({}, path, url);
}
}
private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
}
}
private routeToPath(route: Route, params: RouteParams): string {
const prefix = this.mode === "hash" ? "#" : "";
return (
prefix +
route.path.replace(globalParamRegexp, (match, param) => {
const [key] = param.split(".");
return <string>params[key];
})
);
}
private currentPath(): string {
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
return result || "/";
}
private match(path: string): MatchResult {
for (let routeId of this.routeIds) {
let route = this.routes[routeId];
let params = this.getRouteParams(route, path);
if (params) {
return {
type: "match",
route: route,
params: params,
};
}
}
return { type: "nomatch" };
}
private async matchAndApplyRules(path: string): Promise<MatchResult> {
const result = this.match(path);
if (result.type === "match") {
return this.applyRules(result);
}
return result;
}
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
const route = matchResult.route;
if (route.redirect) {
const path = this.destToPath(route.redirect);
return this.matchAndApplyRules(path);
}
if (route.beforeRouteEnter) {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route,
});
if (result === false) {
return { type: "cancelled" };
} else if (result !== true) {
// we want to navigate to another destination
const path = this.destToPath(result);
return this.matchAndApplyRules(path);
}
}
return matchResult;
}
private getRouteParams(route: Route, path: string): RouteParams | false {
if (route.path === "*") {
return {};
}
if (path.indexOf("?") > -1) {
path = path.split("?")[0];
}
if (path.startsWith("#")) {
path = path.slice(1);
}
const paramsMatch = path.match(route.extractionRegExp);
if (!paramsMatch) {
return false;
}
const result = {};
route.params.forEach((param, index) => {
const [key, suffix] = param.split(".");
const paramValue = paramsMatch[index + 1];
if (suffix === "number") {
return (result[key] = parseInt(paramValue, 10));
}
return (result[key] = paramValue);
});
return result;
}
}
function findParams(str: string): string[] {
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1]);
}
} while (m);
return result;
}
function escapeRegExp(str: string) {
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function makeExtractionRegExp(path: string) {
// replace param strings with capture groups so that we can build a regex to match over the path
const extractionString = path
.split(paramRegexp)
.map((part, index) => {
return index % 2 ? "(.*)" : escapeRegExp(part);
})
.join("");
// Example: /home/{{param1}}/{{param2}} => ^\/home\/(.*)\/(.*)$
return new RegExp(`^${extractionString}$`);
}
-173
View File
@@ -1,173 +0,0 @@
import { Component, Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
/**
* Owl Store
*
* We have here:
* - a Store class
* - useStore hook
* - useDispatch hook
* - useGetters hook
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export interface EnvWithStore extends Env {
store: Store;
}
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 };
}
export class Store extends Context {
actions: any;
env: any;
getters: { [name: string]: (payload?) => any };
updateFunctions: { [key: number]: (() => boolean)[] };
constructor(config: StoreConfig) {
super(config.state);
this.actions = config.actions;
this.env = config.env;
this.getters = {};
this.updateFunctions = [];
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters,
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
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;
}
__notifyComponents(): Promise<void> {
this.trigger("before-update");
return super.__notifyComponents();
}
}
interface SelectorOptions {
store?: Store;
isEqual?: (a: any, b: any) => boolean;
onUpdate?: (result: any) => any;
}
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component = Component.current as Component<any, EnvWithStore>;
const componentId = component.__owl__.id;
const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
let result = selector(store.state, component.props);
const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result);
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[componentId]) {
store.updateFunctions[componentId] = [];
}
function selectCompareUpdate(state, props): boolean {
const oldResult = result;
result = selector(state, props);
const newRevNumber = hashFn(result);
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
revNumber = newRevNumber;
return true;
}
return false;
}
if (options.onUpdate) {
store.on("before-update", component, () => {
const newValue = selector(store!.state, component.props!);
options.onUpdate(newValue);
});
}
store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props);
});
useContextWithCB(store, component, function (): Promise<void> | void {
let shouldRender = false;
for (let fn of store.updateFunctions[componentId]) {
shouldRender = fn() || shouldRender;
}
if (shouldRender) {
return component.render();
}
});
onWillUpdateProps((props) => {
selectCompareUpdate(store.state, props);
});
const __destroy = component.__destroy;
component.__destroy = (parent) => {
delete store.updateFunctions[componentId];
if (options.onUpdate) {
store.off("before-update", component);
}
__destroy.call(component, parent);
};
if (typeof result !== "object" || result === null) {
return result;
}
return new Proxy(result, {
get(target, k) {
return result[k];
},
set(target, k, v) {
throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
},
});
}
export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env as EnvWithStore).store;
return store.dispatch.bind(store);
}
export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env as EnvWithStore).store;
return store.getters;
}
+18 -34
View File
@@ -1,44 +1,28 @@
import { QWeb } from "./qweb/index";
import { registerSheet } from "./component/styles";
import { registerSheet } from "./component/style";
import { globalTemplates } from "./app/template_set";
/**
* Owl Tags
*
* We have here a (very) small collection of tag functions:
*
* - xml
*
* The plan is to add a few other tags such as css, globalcss.
*/
// -----------------------------------------------------------------------------
// Global templates
// -----------------------------------------------------------------------------
/**
* XML tag helper for defining templates. With this, one can simply define
* an inline template with just the template xml:
* ```js
* class A extends Component {
* static template = xml`<div>some template</div>`;
* }
* ```
*/
export function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`;
export function xml(strings: TemplateStringsArray, ...args: any[]) {
const name = `__template__${xml.nextId++}`;
const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
globalTemplates[name] = value;
return name;
}
/**
* CSS tag helper for defining inline stylesheets. With this, one can simply define
* an inline stylesheet with just the following code:
* ```js
* class A extends Component {
* static style = css`.component-a { color: red; }`;
* }
* ```
*/
export function css(strings, ...args) {
const name = `__sheet__${QWeb.nextId++}`;
xml.nextId = 1;
// -----------------------------------------------------------------------------
// Global stylesheets
// -----------------------------------------------------------------------------
export function css(strings: TemplateStringsArray, ...args: any[]) {
const name = `__sheet__${css.nextId++}`;
const value = String.raw(strings, ...args);
registerSheet(name, value);
return name;
}
css.nextId = 1;
+41 -80
View File
@@ -1,103 +1,64 @@
export type Callback = () => void;
/**
* Owl Utils
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* We have here a small collection of utility functions:
*
* - whenReady
* - loadJS
* - loadFile
* - escape
* - debounce
* @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;
}
};
}
import { browser } from "./browser";
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
}
}
export function whenReady(fn?: any) {
export function whenReady(fn?: any): Promise<void> {
return new Promise(function (resolve) {
if (document.readyState !== "loading") {
resolve();
resolve(true);
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function () {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise: Promise<void> = new Promise(function (resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function () {
resolve();
};
script.onerror = function () {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}
export async function loadFile(url: string): Promise<string> {
const result = await browser.fetch(url);
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return await result.text();
}
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
const p = document.createElement("p");
p.textContent = str;
return p.innerHTML;
}
/**
* 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
/*
* This class just transports the fact that a string is safe
* to be injected as HTML. Overriding a JS primitive is quite painful though
* so we need to redfine toString and valueOf.
*/
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout;
return function (this: any) {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
browser.clearTimeout(timeout);
timeout = browser.setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export class Markup extends String {}
export function shallowEqual(p1, p2): boolean {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*/
export function markup(value: any) {
return new Markup(value);
}
-34
View File
@@ -1,34 +0,0 @@
import { VNode, h, addNS } from "./vdom";
const parser = new DOMParser();
export function htmlToVDOM(html: string): VNode[] {
const doc = parser.parseFromString(html, "text/html");
const result: VNode[] = [];
for (let child of doc.body.childNodes) {
result.push(htmlToVNode(child));
}
return result;
}
function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) {
if (node instanceof Comment) {
return h("!", node.textContent);
}
return { text: node.textContent! } as VNode;
}
const attrs = {};
for (let attr of node.attributes) {
attrs[attr.name] = attr.textContent;
}
const children: VNode[] = [];
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
const vnode = h((node as Element).tagName, { attrs }, children);
if (vnode.sel === "svg") {
addNS(vnode.data, (vnode as any).children, vnode.sel);
}
return vnode;
}
-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]);
-247
View File
@@ -1,247 +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) {
if (on[name]) {
invokeHandler(on[name], vnode, event);
} else if (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
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
}
}
}
}
// 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
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
}
}
}
}
}
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 (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
// was `true` and now not provided
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;
-616
View File
@@ -1,616 +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) {
const elm =
vnode.elm ||
(vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, sel)
: api.createElement(sel));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, 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>;
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
if (sel === "dummy") {
// we do not need to add the namespace on dummy elements, they come from a
// subcomponent, which will handle the namespace itself
return;
}
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
const child = children[i];
let childData = child.data;
if (childData !== undefined) {
addNS(childData, (child as VNode).children as VNodes, child.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);
}
}
return vnode(sel, data, children, text, undefined);
}
-200
View File
@@ -1,200 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
if (scope['state'].display) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
if (scope['state'].flag) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1};
let 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
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1};
let 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;
}"
`;
exports[`animations t-transition, on a simple node, not in the DOM 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1};
let 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;
}"
`;
+401
View File
@@ -0,0 +1,401 @@
// 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 } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 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-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 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/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState destroyed component is inactive 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-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 one components can subscribe twice to 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['contextObj1'].a;
let d2 = ctx['contextObj2'].b;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to 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(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to 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-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let d1 = ctx['contextObj'].b;
return block1([d1], [b2]);
}
}"
`;
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 } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].value;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 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-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 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/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].value;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState two components can subscribe to 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-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 independent components on different levels are updated in parallel 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/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].value;
return block1([d1]);
}
}"
`;
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 } = 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(\`Child\`, {}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
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 } = 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(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Parent\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 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/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].value;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 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/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].quantity;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 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-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 d1 = ctx['total'];
let d2 = Object.keys(ctx['state']).length;
return block1([d1, d2], [b2]);
}
}"
`;
exports[`Reactivity: useState very simple use, with initial value 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/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].value;
return block1([d1]);
}
}"
`;
@@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
-452
View File
@@ -1,452 +0,0 @@
import { Component, Env } from "../src/component/component";
import { useRef, useState } from "../src/hooks";
import { QWeb } from "../src/qweb/index";
import { xml } from "../src/tags";
import {
makeDeferred,
makeTestEnv,
makeTestFixture,
nextFrame,
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();
Component.env = env;
qweb = new QWeb();
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("animations", () => {
beforeEach(() => {
cssEl = document.createElement("style");
let css = `
.chimay-enter-active, .chimay-leave-active {
transition-property: opacity;
transition-duration: 0.1s;
}
.chimay-enter, .chimay-leave-to {
opacity: 0;
}
`;
cssEl.textContent = css.trim();
document.head.appendChild(cssEl);
});
afterEach(() => {
document.head.removeChild(cssEl);
unpatchNextFrame();
});
test("t-transition, on a simple node (insert)", async () => {
expect.assertions(5);
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred();
patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
fixture.appendChild(node);
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, on a simple node, not in the DOM", 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"));
// we check here that the css classes have not been removed, since the
// element is not in the dom, we actually do not want to do anything.
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
});
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");
fixture.appendChild(node);
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
await def;
});
test("t-transition on a conditional node", async () => {
expect.assertions(7);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = useState({ hide: false });
}
const widget = new TestWidget();
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
// remove span from the DOM
def = makeDeferred();
widget.state.hide = true;
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-ref", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget();
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with component", async () => {
expect.assertions(5);
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
}
const widget = new Parent();
let def = makeDeferred();
var spanNode;
patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
);
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
});
test("t-transition combined with t-component and t-if", async () => {
expect.assertions(8);
env.qweb.addTemplate(
"Parent",
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
state = useState({ display: true });
}
const widget = new Parent();
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="__3__">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<span t-if="state.flag" t-transition="chimay">blue</span>
</div>
</templates>`
);
class Parent extends Widget {
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent();
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(12);
class Child extends Widget {
static template = xml`<span>blue</span>`;
}
class Parent extends Widget {
static template = xml`
<div t-name="Parent">
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>`;
static components = { Child };
state = useState({ flag: false });
}
const widget = new Parent();
await widget.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
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();
});
// display the span
widget.state.flag = true;
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><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
widget.state.flag = false;
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
widget.state.flag = true;
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><span class="" data-owl-key="__3__">blue</span></div>');
});
test("transitionInsert is called the correct amount of times", async () => {
const oldTransitionInsert = QWeb.utils.transitionInsert;
QWeb.utils.transitionInsert = jest.fn(oldTransitionInsert);
class Child extends Widget {
static template = xml`<span>blue</span>`;
}
class Parent extends Widget {
static template = xml`
<div t-name="Parent">
<Child t-if="state.flag" t-transition="chimay"/>
</div>`;
static components = { Child };
state = useState({ flag: false });
}
patchNextFrame((cb) => cb());
const widget = new Parent();
await widget.mount(fixture);
widget.state.flag = true;
await nextFrame();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false;
await nextFrame();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true;
await nextFrame();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false;
await nextFrame();
widget.state.flag = true;
await nextFrame();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert;
});
});
+265
View File
@@ -0,0 +1,265 @@
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.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);
// });
});
+123
View File
@@ -0,0 +1,123 @@
import { mount, patch, createBlock } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
test("simple attribute", async () => {
const block = createBlock('<div block-attribute-0="hello"></div>');
const tree = block(["world"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
patch(tree, block(["owl"]));
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
});
test("dynamic attribute (pair)", async () => {
const block = createBlock('<div block-attributes="0"></div>');
const tree = block([["hello", "world"]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
patch(tree, block([["ola", "mundo"]]));
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
});
test("dynamic attribute (object)", async () => {
const block = createBlock('<div block-attributes="0"></div>');
const tree = block([{ hello: "world" }]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
patch(tree, block([{ ola: "mundo" }]));
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
});
test("class attribute", async () => {
const block = createBlock('<div block-attribute-0="class"></div>');
const tree = block(["fire"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="fire"></div>`);
patch(tree, block(["water"]));
expect(fixture.innerHTML).toBe(`<div class="water"></div>`);
patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class=""></div>`);
patch(tree, block([0]));
expect(fixture.innerHTML).toBe(`<div class="0"></div>`);
});
test("class attribute with undefined value", async () => {
const block = createBlock('<div block-attribute-0="class"></div>');
const tree = block([undefined]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
});
test("class attribute (with a preexisting value", async () => {
const block = createBlock('<div class="tomato" block-attribute-0="class"></div>');
const tree = block(["potato"]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="tomato potato"></div>`);
patch(tree, block(["squash"]));
expect(fixture.innerHTML).toBe(`<div class="tomato squash"></div>`);
patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
});
describe("properties", () => {
test("input with value attribute", () => {
// render input with initial value
const block = createBlock(`<input block-attribute-0="value"/>`);
const tree = block(["zucchini"]);
mount(tree, fixture);
// const bnode1 = renderToBdom(template, { v: "zucchini" });
// const fixture = makeTestFixture();
// mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("zucchini");
// change value manually in input, to simulate user input
input.value = "tomato";
expect(input.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// input value was properly reset by owl
patch(tree, block(["potato"]));
expect(input.value).toBe("potato");
});
test("input type=checkbox with checked attribute", () => {
// render input with initial value
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
const tree = block([true]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<input type="checkbox">`);
const input = fixture.querySelector("input")!;
expect(input.checked).toBe(true);
});
});
+273
View File
@@ -0,0 +1,273 @@
import { mount, createBlock, multi, config, patch } from "../../src/blockdom";
// import { defaultHandler, setupMainHandler } from "../../src/bdom/block";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let initialHandler = config.mainEventHandler;
beforeEach(() => {
fixture = makeTestFixture();
config.mainEventHandler = initialHandler;
});
afterEach(() => {
fixture.remove();
});
test("simple event handling, with function", async () => {
const block = createBlock('<div block-handler-0="click"></div>');
let n = 0;
const tree = block([() => n++]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(fixture.firstChild).toBeInstanceOf(HTMLDivElement);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(n).toBe(1);
});
test("simple event handling, with function and argument", async () => {
const block = createBlock('<div block-handler-0="click"></div>');
let n = 0;
const onClick = (arg: number) => {
n += arg;
};
const tree = block([[onClick, 3]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(fixture.firstChild).toBeInstanceOf(HTMLDivElement);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(n).toBe(3);
patch(tree, block([[onClick, 5]]));
(fixture.firstChild as HTMLDivElement).click();
expect(n).toBe(8);
});
test("simple event handling ", async () => {
config.mainEventHandler = (data, ev) => {
if (typeof data === "function") {
data();
} else {
const [owner, method] = data;
owner[method]();
}
return false;
};
const block = createBlock('<div block-handler-0="click"></div>');
let n = 0;
const obj = { f: () => n++ };
const tree = block([[obj, "f"]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(fixture.firstChild).toBeInstanceOf(HTMLDivElement);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(n).toBe(1);
});
test("can bind two handlers on same node", async () => {
const block = createBlock('<div block-handler-0="click" block-handler-1="dblclick"></div>');
let steps: string[] = [];
let handleClick = () => steps.push("click");
let handleDblClick = () => steps.push("dblclick");
const tree = block([handleClick, handleDblClick]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
(fixture.firstChild as HTMLDivElement).click();
(fixture.firstChild as HTMLDivElement).dispatchEvent(new Event("dblclick", { bubbles: true }));
expect(steps).toEqual(["click", "dblclick"]);
});
test("two same block nodes with different handlers", async () => {
const block = createBlock('<div block-handler-0="click"></div>');
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = multi([block([handler1]), block([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div><div></div>");
(fixture.firstChild as HTMLDivElement).click();
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
});
test("two same block nodes with different handlers (synthetic)", async () => {
const block = createBlock('<div block-handler-0="click.synthetic"></div>');
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = multi([block([handler1]), block([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div><div></div>");
(fixture.firstChild as HTMLDivElement).click();
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
});
test("two event handlers on same event", async () => {
const block = createBlock('<div block-handler-0="click" block-handler-1="click"></div>');
let n = 0;
let m = 0;
const tree = block([() => m++, () => n++]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(m).toBe(0);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(m).toBe(1);
expect(n).toBe(1);
});
test("two synthetic event handlers on same event", async () => {
const block = createBlock(
'<div block-handler-0="click.synthetic" block-handler-1="click.synthetic"></div>'
);
let n = 0;
let m = 0;
const tree = block([() => m++, () => n++]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(m).toBe(0);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(m).toBe(1);
expect(n).toBe(1);
});
test("synthetic and native handlers can cohabitate", async () => {
const block = createBlock(
'<div block-handler-0="click.synthetic"><div block-handler-1="click"/></div>'
);
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div></div>");
(fixture.firstChild!.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1"]);
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1", "1"]);
});
test("synthetic and native handlers can cohabitate (2)", async () => {
const block = createBlock(
'<div block-handler-0="click"><div block-handler-1="click.synthetic"/></div>'
);
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div></div>");
(fixture.firstChild!.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["1", "2", "1"]);
});
test("synthetic and native handlers can cohabitate (3)", async () => {
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
const block = createBlock('<div block-handler-0="click"/>');
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
let steps: string[] = [];
const handler0 = () => steps.push("0");
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
const children = fixture.children[0].children;
(children[0] as HTMLElement).click();
expect(steps).toEqual(["1", "0"]);
(children[1] as HTMLElement).click();
expect(steps).toEqual(["1", "0", "0", "2"]);
});
test("synthetic and native handlers can cohabitate (4)", async () => {
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
const block = createBlock('<div block-handler-0="click"/>');
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
let steps: string[] = [];
const handler0 = (ev: Event) => {
steps.push("0");
ev.stopPropagation();
};
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
const children = fixture.children[0].children;
(children[0] as HTMLElement).click();
expect(steps).toEqual(["1", "0"]);
(children[1] as HTMLElement).click();
expect(steps).toEqual(["1", "0", "0"]);
});
test("synthetic and native handlers can cohabitate (5)", async () => {
const block = createBlock('<div block-handler-0="click.synthetic" block-handler-1="click"/>');
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1"]);
});
test("synthetic and native handlers can cohabitate (6)", async () => {
const block = createBlock(`<div
block-handler-0="click.synthetic"
block-handler-1="click"
block-handler-2="click.synthetic"
block-handler-3="click"
/>`);
let steps: string[] = [];
const handler1 = () => steps.push("1");
const handler2 = () => steps.push("2");
const handler3 = () => steps.push("3");
const handler4 = () => steps.push("4");
const tree = block([handler1, handler2, handler3, handler4]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "4", "1", "3"]);
});
+57
View File
@@ -0,0 +1,57 @@
import { createBlock, mount, patch, remove } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
test("simple callback ref", async () => {
const block = createBlock('<div><span block-ref="0">hey</span></div>');
let arg: any = undefined;
let n = 0;
const refFn = (_arg: any) => {
n++;
arg = _arg;
};
const tree = block([refFn]);
expect(arg).toBeUndefined();
expect(n).toBe(0);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
expect(n).toBe(1);
expect(arg).toBeInstanceOf(HTMLSpanElement);
expect(arg!.innerHTML).toBe("hey");
patch(tree, block([refFn]));
expect(n).toBe(1);
remove(tree);
expect(fixture.innerHTML).toBe("");
expect(arg).toBeNull();
expect(n).toBe(2);
});
test("is in dom when callback is called", async () => {
expect.assertions(1);
const block = createBlock('<div><span block-ref="0">hey</span></div>');
const refFn = (span: any) => {
expect(document.body.contains(span)).toBeTruthy();
};
const tree = block([refFn]);
mount(tree, fixture);
});
+11
View File
@@ -0,0 +1,11 @@
let lastFixture: any = null;
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture;
}
+48
View File
@@ -0,0 +1,48 @@
import { html, mount, patch, text } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("html block", () => {
test("can be mounted and patched", async () => {
const tree = html("<span>1</span><span>2</span>");
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<span>1</span><span>2</span>");
patch(tree, html("<div>coucou</div>"));
expect(fixture.innerHTML).toBe("<div>coucou</div>");
});
test("html vnode can be used as text", () => {
mount(text(html("<p>a</p>") as any), fixture);
expect(fixture.textContent).toBe("<p>a</p>");
});
test("html vnode can represent <tr>", () => {
const fixture = document.createElement("table");
// const block = createBlock('<table><block-child-0/></table>');
// const tree = block([], [html(`<tr><td>tomato</td></tr>`)]);
const tree = html(`<tr><td>tomato</td></tr>`);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tr><td>tomato</td></tr>");
patch(tree, html(`<tr><td>potato</td></tr>`));
expect(fixture.innerHTML).toBe("<tr><td>potato</td></tr>");
});
});
+458
View File
@@ -0,0 +1,458 @@
import { list, mount, multi, patch, text, createBlock, VNode, withKey } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
function kText(str: string, key: any): VNode {
return withKey(text(str), key);
}
function n(n: number) {
return kText(String(n), n);
}
const span = createBlock("<span><block-text-0/></span>");
const p = createBlock("<p><block-text-0/></p>");
function kSpan(str: string, key: any): VNode {
return withKey(span([str]), key);
}
function kPair(n: number): VNode {
const bnodes = [p([String(n)]), p([String(n)])];
return withKey(multi(bnodes), n);
}
describe("list node: misc", () => {
test("list node", async () => {
const bnodes = [1, 2, 3].map((key) => kText(`text${key}`, key));
const tree = list(bnodes);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("text1text2text3");
});
test("list vnode can be used as text", () => {
mount(text(list([text("a"), text("b")]) as any), fixture);
expect(fixture.innerHTML).toBe("ab");
});
test("a list block can be removed and leaves nothing", async () => {
const bnodes = [
{ id: 1, name: "sheep" },
{ id: 2, name: "cow" },
].map((elem) => kText(elem.name, elem.id));
const tree = list(bnodes);
expect(fixture.childNodes.length).toBe(0);
mount(tree, fixture);
expect(fixture.childNodes.length).toBe(3);
expect(fixture.innerHTML).toBe("sheepcow");
tree.remove();
expect(fixture.innerHTML).toBe("");
expect(fixture.childNodes.length).toBe(0);
});
test("patching a list block inside an elem block", async () => {
const block = createBlock("<div><block-child-0/></div>");
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
patch(tree, block([], [list([1, 2, 3].map(n))]));
expect(fixture.innerHTML).toBe("<div>123</div>");
patch(tree, block([], [list([])]));
expect(fixture.innerHTML).toBe("<div></div>");
patch(tree, block([], [list([1, 2, 3].map(n))]));
expect(fixture.innerHTML).toBe("<div>123</div>");
});
test("list of lists", async () => {
const tree = list([
withKey(list([kText("a1", "1"), kText("a2", "2")]), "a"),
withKey(list([kText("b1", "1"), kText("b2", "2")]), "b"),
]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("a1a2b1b2");
patch(
tree,
list([
withKey(list([kText("b1", "1"), kText("b2", "2")]), "b"),
withKey(list([kText("a1", "1"), kText("a2", "2")]), "a"),
])
);
expect(fixture.innerHTML).toBe("b1b2a1a2");
patch(
tree,
list([
withKey(list([kText("a2", "2"), kText("a1", "1")]), "a"),
withKey(list([kText("b2", "2"), kText("b1", "1")]), "b"),
])
);
expect(fixture.innerHTML).toBe("a2a1b2b1");
});
});
describe("adding/removing elements", () => {
test("removing elements", () => {
const tree = list([kText("a", "a")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("a");
patch(tree, list([]));
expect(fixture.innerHTML).toBe("");
});
test("removing 1 elements from 2", () => {
const tree = list([kText("a", "a"), kText("b", "b")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("ab");
patch(tree, list([kText("a", "a")]));
expect(fixture.innerHTML).toBe("a");
});
test("removing elements", () => {
const tree = list([kSpan("a", "a")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<span>a</span>");
patch(tree, list([]));
expect(fixture.innerHTML).toBe("");
});
test("removing elements, variation", () => {
const f = (i: number) => {
const b = multi([span([`a${i}`]), span([`b${i}`])]);
b.key = i;
return b;
};
const tree = list([f(1), f(2)]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<span>a1</span><span>b1</span><span>a2</span><span>b2</span>");
patch(tree, list([]));
expect(fixture.innerHTML).toBe("");
});
test("adding one element at the end", () => {
const tree = list([n(1), n(2)]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12");
patch(tree, list([n(1), n(2), n(3)]));
expect(fixture.innerHTML).toBe("123");
});
test("adding one element at the end", () => {
const tree = list([n(1)]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("1");
patch(tree, list([n(1), n(2), n(3)]));
expect(fixture.innerHTML).toBe("123");
});
test("prepend elements: 4,5 => 1,2,3,4,5", () => {
const tree = list([n(4), n(5)]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("45");
patch(tree, list([n(1), n(2), n(3), n(4), n(5)]));
expect(fixture.innerHTML).toBe("12345");
});
test("prepend elements: 4,5 => 1,2,3,4,5 (with multi)", () => {
const tree = list([kPair(4), kPair(5)]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>4</p><p>4</p><p>5</p><p>5</p>");
patch(tree, list([1, 2, 3, 4, 5].map(kPair)));
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
});
test("add element in middle: 1,2,4,5 => 1,2,3,4,5", () => {
const tree = list([1, 2, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("1245");
patch(tree, list([1, 2, 3, 4, 5].map(n)));
expect(fixture.innerHTML).toBe("12345");
});
test("add element in middle: 1,2,4,5 => 1,2,3,4,5 (multi)", () => {
const tree = list([1, 2, 4, 5].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
patch(tree, list([1, 2, 3, 4, 5].map(kPair)));
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
});
test("add element at beginning and end: 2,3,4 => 1,2,3,4,5", () => {
const tree = list([2, 3, 4].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("234");
patch(tree, list([1, 2, 3, 4, 5].map(n)));
expect(fixture.innerHTML).toBe("12345");
});
test("add element at beginning and end: 2,3,4 => 1,2,3,4,5 (multi)", () => {
const tree = list([2, 3, 4].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p>");
patch(tree, list([1, 2, 3, 4, 5].map(kPair)));
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
});
test("adds children: [] => [1,2,3]", () => {
const tree = list([].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("");
patch(tree, list([1, 2, 3].map(n)));
expect(fixture.innerHTML).toBe("123");
});
test("adds children: [] => [1,2,3] (multi)", () => {
const tree = list([].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("");
patch(tree, list([1, 2, 3].map(kPair)));
expect(fixture.innerHTML).toBe("<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p>");
});
test("adds children: [] => [1,2,3] (inside elem)", () => {
const block = createBlock("<p><block-child-0/></p>");
const tree = block([], []);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p></p>");
patch(tree, block([], [list([1, 2, 3].map(n))]));
expect(fixture.innerHTML).toBe("<p>123</p>");
});
test("adds children: [] => [1,2,3] (inside elem, multi)", () => {
const block = createBlock("<p><block-child-0/></p>");
const tree = block([], []);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p></p>");
patch(tree, block([], [list([1, 2, 3].map(kPair))]));
expect(fixture.innerHTML).toBe("<p><p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p></p>");
});
test("remove children: [1,2,3] => []", () => {
const tree = list([1, 2, 3].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("123");
patch(tree, list([].map(n)));
expect(fixture.innerHTML).toBe("");
});
test("remove children: [1,2,3] => [] (multi)", () => {
const tree = list([1, 2, 3].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p>");
patch(tree, list([].map(kPair)));
expect(fixture.innerHTML).toBe("");
});
test("remove children: [1,2,3] => [] (inside elem)", () => {
const block = createBlock("<p><block-child-0/></p>");
const tree = block([], [list([1, 2, 3].map(n))]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>123</p>");
patch(tree, block([], [list([])]));
expect(fixture.innerHTML).toBe("<p></p>");
});
test("remove children from the beginning: [1,2,3,4,5] => [3,4,5]", () => {
const tree = list([1, 2, 3, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12345");
patch(tree, list([3, 4, 5].map(n)));
expect(fixture.innerHTML).toBe("345");
});
test("remove children from the beginning: [1,2,3,4,5] => [3,4,5] (multi)", () => {
const tree = list([1, 2, 3, 4, 5].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
patch(tree, list([3, 4, 5].map(kPair)));
expect(fixture.innerHTML).toBe("<p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>");
});
test("remove children from the end: [1,2,3,4,5] => [1,2,3]", () => {
const tree = list([1, 2, 3, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12345");
patch(tree, list([1, 2, 3].map(n)));
expect(fixture.innerHTML).toBe("123");
});
test("remove children from the middle: [1,2,3,4,5] => [1,2,4,5]", () => {
const tree = list([1, 2, 3, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12345");
patch(tree, list([1, 2, 4, 5].map(n)));
expect(fixture.innerHTML).toBe("1245");
});
});
describe("element reordering", () => {
test("move element forward: [1,2,3,4] => [2,3,1,4]", () => {
const tree = list([1, 2, 3, 4].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("1234");
patch(tree, list([2, 3, 1, 4].map(n)));
expect(fixture.innerHTML).toBe("2314");
});
test("move element forward: [1,2,3,4] => [2,3,1,4] (multi)", () => {
const tree = list([1, 2, 3, 4].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p>"
);
patch(tree, list([2, 3, 1, 4].map(kPair)));
expect(fixture.innerHTML).toBe(
"<p>2</p><p>2</p><p>3</p><p>3</p><p>1</p><p>1</p><p>4</p><p>4</p>"
);
});
test("move element to end: [1,2,3] => [2,3,1]", () => {
const tree = list([1, 2, 3].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("123");
patch(tree, list([2, 3, 1].map(n)));
expect(fixture.innerHTML).toBe("231");
});
test("move element to end: [1,2,3] => [2,3,1] (multi)", () => {
const tree = list([1, 2, 3].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p>");
patch(tree, list([2, 3, 1].map(kPair)));
expect(fixture.innerHTML).toBe("<p>2</p><p>2</p><p>3</p><p>3</p><p>1</p><p>1</p>");
});
test("move element backward: [1,2,3,4] => [1,4,2,3]", () => {
const tree = list([1, 2, 3, 4].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("1234");
patch(tree, list([1, 4, 2, 3].map(n)));
expect(fixture.innerHTML).toBe("1423");
});
test("swaps first and last: [1,2,3,4] => [4,3,2,1]", () => {
const tree = list([1, 2, 3, 4].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("1234");
patch(tree, list([4, 3, 2, 1].map(n)));
expect(fixture.innerHTML).toBe("4321");
});
});
describe("miscellaneous operations", () => {
test("move to left and replace: [1,2,3,4,5] => [4,1,2,3,6]", () => {
const tree = list([1, 2, 3, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12345");
patch(tree, list([4, 1, 2, 3, 6].map(n)));
expect(fixture.innerHTML).toBe("41236");
});
test("move to left and replace: [1,2,3,4,5] => [4,1,2,3,6] (multi)", () => {
const tree = list([1, 2, 3, 4, 5].map(kPair));
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>4</p><p>4</p><p>5</p><p>5</p>"
);
patch(tree, list([4, 1, 2, 3, 6].map(kPair)));
expect(fixture.innerHTML).toBe(
"<p>4</p><p>4</p><p>1</p><p>1</p><p>2</p><p>2</p><p>3</p><p>3</p><p>6</p><p>6</p>"
);
});
test("move to left and leave hole: [1,4,5] => [4,6]", () => {
const tree = list([1, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("145");
patch(tree, list([4, 6].map(n)));
expect(fixture.innerHTML).toBe("46");
});
test("[2,4,5] => [4,5,3]", () => {
const tree = list([2, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("245");
patch(tree, list([4, 5, 3].map(n)));
expect(fixture.innerHTML).toBe("453");
});
test("reverse elements [1,2,3,4,5,6,7,8] => [8,7,6,5,4,3,2,1]", () => {
const tree = list([1, 2, 3, 4, 5, 6, 7, 8].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("12345678");
patch(tree, list([8, 7, 6, 5, 4, 3, 2, 1].map(n)));
expect(fixture.innerHTML).toBe("87654321");
});
test("some permutation [0,1,2,3,4,5] => [4,3,2,1,5,0]", () => {
const tree = list([0, 1, 2, 3, 4, 5].map(n));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("012345");
patch(tree, list([4, 3, 2, 1, 5, 0].map(n)));
expect(fixture.innerHTML).toBe("432150");
});
});
+79
View File
@@ -0,0 +1,79 @@
import { mount, multi, patch, text, createBlock } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("multi blocks", () => {
test("multiblock with 2 text blocks", async () => {
const tree = multi([text("foo"), text("bar")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foobar");
});
test("a multiblock can be removed and leaves no extra text nodes", async () => {
const block1 = createBlock("<div>foo</div>");
const block2 = createBlock("<span>bar</span>");
const tree = multi([block1(), block2()]);
expect(fixture.childNodes.length).toBe(0);
mount(tree, fixture);
expect(fixture.childNodes.length).toBe(2);
tree.remove();
expect(fixture.childNodes.length).toBe(0);
});
test("multiblock with an empty children", async () => {
const block = createBlock("<div>foo</div>");
const tree = multi([block(), undefined]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("multi block in a regular block", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<span>yip yip</span>");
const tree = block1([], [multi([block2()])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><span>yip yip</span></div>");
});
test("patching a multiblock ", async () => {
const tree = multi([text("foo"), text("bar")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foobar");
patch(tree, multi([text("blip"), text("bar")]));
expect(fixture.innerHTML).toBe("blipbar");
});
test("simple multi with multiple roots", async () => {
const block1 = createBlock("<div>foo</div>");
const block2 = createBlock("<span>bar</span>");
const tree = multi([block1(), block2()]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div>foo</div><span>bar</span>");
});
test("multi vnode can be used as text", () => {
mount(text(multi([text("a"), text("b")]) as any), fixture);
expect(fixture.innerHTML).toBe("ab");
});
});
+72
View File
@@ -0,0 +1,72 @@
import { createBlock, mount } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
const XHTML_URI = "http://www.w3.org/1999/xhtml";
const SVG_URI = "http://www.w3.org/2000/svg";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("namespace", () => {
test("default namespace is xhtml", () => {
const block = createBlock(`<tag/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
});
test("namespace can be changed with block-ns", () => {
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
});
test("namespace is kept for children", () => {
const block = createBlock(
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<parent><child><subchild></subchild></child><child></child></parent>"
);
const parent = fixture.firstElementChild!;
const child1 = parent.firstElementChild!;
const subchild = child1.firstElementChild!;
const child2 = child1.nextElementSibling!;
expect(parent.namespaceURI).toBe(SVG_URI);
expect(child1.namespaceURI).toBe(SVG_URI);
expect(child2.namespaceURI).toBe(SVG_URI);
expect(subchild.namespaceURI).toBe(SVG_URI);
});
test("various namespaces in same block", () => {
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
const none = fixture.firstElementChild!;
const one = none.firstElementChild!;
const two = one.nextElementSibling!;
expect(none.namespaceURI).toBe(XHTML_URI);
expect(one.namespaceURI).toBe("one");
expect(two.namespaceURI).toBe("two");
});
});
+84
View File
@@ -0,0 +1,84 @@
import { createBlock, mount, multi, patch } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("before remove is called", () => {
test("simple removal", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<p>coucou</p>");
const child = block2();
const tree = block1([], [child]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>coucou</p></div>");
let n = 1;
child.beforeRemove = () => n++;
patch(tree, block1([], []), true);
expect(fixture.innerHTML).toBe("<div></div>");
expect(n).toBe(2);
});
test("removal, variation with grandchild", async () => {
const block1 = createBlock("<p><block-child-0/></p>");
const block2 = createBlock("<span>coucou</span>");
const child = block2();
const tree = block1([], [block1([], [child])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p><p><span>coucou</span></p></p>");
let n = 1;
child.beforeRemove = () => n++;
patch(tree, block1([], []), true);
expect(fixture.innerHTML).toBe("<p></p>");
expect(n).toBe(2);
});
test("remove a child of a multi", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<p>coucou</p>");
const child1 = block2();
const child2 = block2();
const tree = block1([], [multi([child1, child2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>coucou</p><p>coucou</p></div>");
let n = 1;
child1.beforeRemove = () => n++;
patch(tree, block1([], [multi([])]), true);
expect(fixture.innerHTML).toBe("<div></div>");
expect(n).toBe(2);
});
test("remove a multi", async () => {
const block1 = createBlock("<div><block-child-0/></div>");
const block2 = createBlock("<p>coucou</p>");
const child1 = block2();
const child2 = block2();
const tree = block1([], [multi([child1, child2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><p>coucou</p><p>coucou</p></div>");
let n = 1;
child1.beforeRemove = () => n++;
patch(tree, block1([], []), true);
expect(fixture.innerHTML).toBe("<div></div>");
expect(n).toBe(2);
});
});
+69
View File
@@ -0,0 +1,69 @@
import { 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 text", () => {
test("simple text node", async () => {
const tree = text("foo");
expect(tree.el).toBe(undefined);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foo");
expect(tree.el).not.toBe(undefined);
});
test("patching a simple text node", async () => {
const tree = text("foo");
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foo");
patch(tree, text("bar"));
expect(fixture.innerHTML).toBe("bar");
});
test("falsy values in text nodes", () => {
const cases = [
[false, "false"],
[undefined, ""],
[null, ""],
[0, "0"],
["", ""],
];
for (let [value, result] of cases) {
const fixture = makeTestFixture();
mount(text(value as any), fixture);
expect(fixture.innerHTML).toBe(result);
}
});
test("vtext node can be used as text", () => {
const t = text("foo") as any;
mount(text(t), fixture);
expect(fixture.innerHTML).toBe("foo");
});
});
describe("remove text blocks", () => {
test("a text block can be removed", async () => {
const tree = text("cat");
expect(fixture.childNodes.length).toBe(0);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("cat");
expect(fixture.childNodes.length).toBe(1);
remove(tree);
expect(fixture.innerHTML).toBe("");
expect(fixture.childNodes.length).toBe(0);
});
});
+114
View File
@@ -0,0 +1,114 @@
import { createBlock, mount, patch, text, toggler } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("togglers", () => {
test("can mount and update text nodes", async () => {
const tree = toggler("key", text("foo"));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foo");
const textnode = fixture.firstChild!;
expect(textnode.textContent).toBe("foo");
patch(tree, toggler("key", text("bar")));
expect(fixture.innerHTML).toBe("bar");
expect(fixture.firstChild).toBe(textnode);
});
test("can toggle between two text nodes (different key)", async () => {
const tree = toggler("key1", text("foo"));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foo");
const textnode = fixture.firstChild!;
expect(textnode.textContent).toBe("foo");
patch(tree, toggler("key2", text("bar")));
expect(fixture.innerHTML).toBe("bar");
expect(fixture.firstChild).not.toBe(textnode);
// we check here that the key was properly reset
const other = fixture.firstChild;
patch(tree, toggler("key1", text("foo")));
expect(fixture.innerHTML).toBe("foo");
expect(fixture.firstChild).not.toBe(other);
});
test("can toggle between text node and block", async () => {
const block = createBlock("<p>hey</p>");
const tree = toggler("key1", text("foo"));
mount(tree, fixture);
expect(fixture.innerHTML).toBe("foo");
const textnode = fixture.firstChild!;
patch(tree, toggler("key2", block()));
expect(fixture.innerHTML).toBe("<p>hey</p>");
expect(fixture.firstChild).not.toBe(textnode);
});
test("beforeRemove is called", () => {
const block = createBlock("<p>hey</p>");
function blockOverriden() {
const vn = block();
vn.beforeRemove = () => {
steps.push("beforeRemove");
};
return vn;
}
const steps: string[] = [];
const tree = toggler("key1", blockOverriden());
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>hey</p>");
patch(tree, toggler("key2", text("foo")));
expect(fixture.innerHTML).toBe("foo");
patch(tree, toggler("key3", blockOverriden()), true);
expect(fixture.innerHTML).toBe("<p>hey</p>");
patch(tree, toggler("key2", text("foo")), true);
expect(fixture.innerHTML).toBe("foo");
expect(steps).toEqual(["beforeRemove"]);
});
test("beforeRemove is called when within a block", () => {
const block = createBlock("<p><block-child-0/></p>");
const conditionalBlock = createBlock("<p>hey</p>");
const steps: string[] = [];
function blockOverriden() {
const vn = conditionalBlock();
vn.beforeRemove = function () {
steps.push("beforeRemove");
};
return vn;
}
const mainBlock = block([], [toggler("key1", blockOverriden())]);
mount(mainBlock, fixture);
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
patch(mainBlock, block([], []));
expect(fixture.innerHTML).toBe("<p></p>");
patch(mainBlock, block([], [toggler("key1", blockOverriden())]), true);
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
patch(mainBlock, block([], []), true);
expect(fixture.innerHTML).toBe("<p></p>");
expect(steps).toEqual(["beforeRemove"]);
});
});
@@ -0,0 +1,712 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`attributes changing a class with t-att-class (preexisting class 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 class=\\"hoy\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`attributes changing a class with t-att-class 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`attributes changing an attribute with t-att- 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-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`attributes class and t-att-class should combine together 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-attribute-0=\\"class\\" class=\\"hello\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes class and t-attf-class with ternary operation 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 class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = (ctx['value']?'world':'');
return block1([d1]);
}
}"
`;
exports[`attributes dynamic attribute evaluating to 0 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic attribute falsy variable 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic attribute with a dash 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-attribute-0=\\"data-action-id\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['id'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic attributes 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = 'bar';
return block1([d1]);
}
}"
`;
exports[`attributes dynamic class attribute 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['c'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic class attribute evaluating to 0 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic empty class attribute 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['c'];
return block1([d1]);
}
}"
`;
exports[`attributes dynamic formatted attributes with a dash 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-attribute-0=\\"aria-label\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`Some text \${ctx['id']}\`;
return block1([d1]);
}
}"
`;
exports[`attributes fixed variable 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes format expression 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = (ctx['value']+37);
return block1([d1]);
}
}"
`;
exports[`attributes format literal 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`bar\`;
return block1([d1]);
}
}"
`;
exports[`attributes format multiple 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`a \${ctx['value1']} is \${ctx['value2']} of \${ctx['value3']} ]\`;
return block1([d1]);
}
}"
`;
exports[`attributes format value 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-attribute-0=\\"foo\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`b\${ctx['value']}r\`;
return block1([d1]);
}
}"
`;
exports[`attributes from object variables set previously 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><span block-attribute-0=\\"class\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"o\\", {a:'b'});
let d1 = ctx['o'].a;
return block1([d1]);
}
}"
`;
exports[`attributes from variables set previously (no external node) 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"abc\\", 'def');
let d1 = ctx['abc'];
return block1([d1]);
}
}"
`;
exports[`attributes from variables set previously 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><span block-attribute-0=\\"class\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"abc\\", 'def');
let d1 = ctx['abc'];
return block1([d1]);
}
}"
`;
exports[`attributes object 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-attributes=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes static attributes 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 foo=\\"a\\" bar=\\"b\\" baz=\\"c\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`attributes static attributes on void elements 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(\`<img src=\\"/test.skip.jpg\\" alt=\\"Test\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`attributes static attributes with dashes 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 aria-label=\\"Close\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`attributes t-att-class and class should combine together 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 class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes t-att-class with multiple classes 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = {'a b c':ctx['value']};
return block1([d1]);
}
}"
`;
exports[`attributes t-att-class with multiple classes 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = {['a b c']:ctx['value']};
return block1([d1]);
}
}"
`;
exports[`attributes t-att-class with multiple classes, some of which are duplicate 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = {'a b c':ctx['value'],'a b d':!ctx['value']};
return block1([d1]);
}
}"
`;
exports[`attributes t-att-class with object 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 class=\\"static\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = {a:ctx['b'],c:ctx['d'],e:ctx['f']};
return block1([d1]);
}
}"
`;
exports[`attributes t-attf-class 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`hello\`;
return block1([d1]);
}
}"
`;
exports[`attributes t-attf-class should combine with class 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 class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`world\`;
return block1([d1]);
}
}"
`;
exports[`attributes t-attf-class with multiple classes 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`hello \${ctx['word']}\`;
return block1([d1]);
}
}"
`;
exports[`attributes t-attf-class with multiple classes separated by multiple spaces 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-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = \`hello \${ctx['word']}\`;
return block1([d1]);
}
}"
`;
exports[`attributes tuple literal 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-attributes=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ['foo','bar'];
return block1([d1]);
}
}"
`;
exports[`attributes tuple variable 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-attributes=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`attributes two classes 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 class=\\"a b\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`attributes two dynamic attributes 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-attribute-0=\\"foo\\" block-attribute-1=\\"bar\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = 'bar';
let d2 = 'foo';
return block1([d1, d2]);
}
}"
`;
exports[`attributes updating classes (with obj notation) 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 class=\\"hoy\\" block-attribute-0=\\"class\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = {'a b':ctx['condition']};
return block1([d1]);
}
}"
`;
exports[`attributes various escapes 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 foo=\\"&lt;foo\\" block-attribute-0=\\"bar\\" block-attribute-1=\\"baz\\" block-attributes=\\"2\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['bar'];
let d2 = \`<\${ctx['baz']}>\`;
let d3 = ctx['qux'];
return block1([d1, d2, d3]);
}
}"
`;
exports[`attributes various escapes 2 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> &lt; </div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`special cases for some specific html attributes/properties input of type checkbox with t-att-indeterminate 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(\`<input type=\\"checkbox\\" block-attribute-0=\\"indeterminate\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties input type= checkbox, with t-att-checked 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(\`<input type=\\"checkbox\\" block-attribute-0=\\"checked\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['flag'];
return block1([d1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties input with t-att-value 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(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties select with t-att-value 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(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['value'];
return block1([d1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties textarea with t-att-value 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(\`<textarea block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['v'];
return block1([d1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties various boolean html attributes 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><input type=\\"checkbox\\" checked=\\"checked\\"/><input checked=\\"checked\\"/><div checked=\\"checked\\"/><div selected=\\"selected\\"/><option selected=\\"selected\\" other=\\"1\\"/><input readonly=\\"readonly\\"/><button disabled=\\"disabled\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`comments only a comment 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(\`<!-- comment-->\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`comments properly handle comments 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>hello <!-- comment-->owl</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`comments properly handle comments between t-if/t-else 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-child-0/><block-child-1/></div>\`);
let block2 = createBlock(\`<span>true</span>\`);
let block3 = createBlock(\`<span>owl</span>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (true) {
b2 = block2();
} else {
b3 = block3();
}
return block1([], [b2, b3]);
}
}"
`;
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`error handling cannot add twice the same template 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/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
@@ -0,0 +1,579 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-on can bind event handler 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on can bind handlers with arguments 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [()=>v1(5), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on can bind handlers with empty object 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['doSomething'];
let d1 = [()=>v1({}), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on can bind handlers with empty object (with non empty inner string) 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['doSomething'];
let d1 = [()=>v1({}), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on can bind handlers with empty object (with non empty inner string) 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(\`<ul><block-child-0/></ul>\`);
let block3 = createBlock(\`<li><a block-handler-0=\\"click\\">link</a></li>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['someval']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`action\`] = v_block2[i1];
ctx[\`action_index\`] = i1;
let key1 = ctx['action_index'];
const v1 = ctx['activate'];
const v2 = ctx['action'];
let d1 = [()=>v1(v2), ctx];
c_block2[i1] = withKey(block3([d1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-on can bind handlers with object arguments 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [()=>v1({val:5}), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on can bind two event handlers 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(\`<button block-handler-0=\\"click\\" block-handler-1=\\"dblclick\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['handleClick'];
let d1 = [v1, ctx];
const v2 = ctx['handleDblClick'];
let d2 = [v2, ctx];
return block1([d1, d2]);
}
}"
`;
exports[`t-on handler is bound to proper owner 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 2 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 block2 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`value\`] = v_block1[i1];
let key1 = ctx['value'];
const v1 = ctx['add'];
let d1 = [v1, ctx];
c_block1[i1] = withKey(block2([d1]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 3 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 3 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;
const callTemplate_2 = getTemplate(\`sub\`);
return function template(ctx, node, key = \\"\\") {
return callTemplate_2.call(this, ctx, node, key + \`__1\`);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 4 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 4 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;
const callTemplate_2 = getTemplate(\`sub\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`value\`] = v_block1[i1];
ctx[\`value_first\`] = i1 === 0;
ctx[\`value_last\`] = i1 === v_block1.length - 1;
ctx[\`value_index\`] = i1;
ctx[\`value_value\`] = k_block1[i1];
let key1 = ctx['value'];
c_block1[i1] = withKey(callTemplate_2.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-on receive event in first argument 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) basic support for native listener 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 class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['divClicked'];
let d1 = [v1, ctx];
const v2 = ctx['btnClicked'];
let d2 = [v2, ctx];
return block1([d1, d2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on combined with t-esc 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><button block-handler-0=\\"click\\"><block-text-1/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
let d2 = ctx['text'];
return block1([d1, d2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on combined with t-out 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><button block-handler-0=\\"click\\"><block-child-0/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
let b2 = safeOutput(ctx['html']);
return block1([d1], [b2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with .capture modifier 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-handler-0=\\"click.capture\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onCapture'];
let d1 = [\\"capture\\", v1, ctx];
const v2 = ctx['doSomething'];
let d2 = [v2, ctx];
return block1([d1, d2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with empty handler (only modifiers) 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><button block-handler-0=\\"click.prevent\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [\\"prevent\\", , ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent and self modifiers (order matters) 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><button block-handler-0=\\"click.prevent.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick'];
let d1 = [\\"prevent\\",\\"self\\", v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent and/or stop modifiers 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><button block-handler-0=\\"click.prevent\\">Button 1</button><button block-handler-1=\\"click.stop\\">Button 2</button><button block-handler-2=\\"click.prevent.stop\\">Button 3</button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClickPrevented'];
let d1 = [\\"prevent\\", v1, ctx];
const v2 = ctx['onClickStopped'];
let d2 = [\\"stop\\", v2, ctx];
const v3 = ctx['onClickPreventedAndStopped'];
let d3 = [\\"prevent\\",\\"stop\\", v3, ctx];
return block1([d1, d2, d3]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent modifier in t-foreach 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-child-0/></div>\`);
let block3 = createBlock(\`<a href=\\"#\\" block-handler-0=\\"click.prevent\\"> Edit <block-text-1/></a>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['projects']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`project\`] = v_block2[i1];
let key1 = ctx['project'];
const v1 = ctx['onEdit'];
const v2 = ctx['project'];
let d1 = [\\"prevent\\", ev=>v1(v2.id,ev), ctx];
let d2 = ctx['project'].name;
c_block2[i1] = withKey(block3([d1, d2]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with self and prevent modifiers (order matters) 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><button block-handler-0=\\"click.self.prevent\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick'];
let d1 = [\\"self\\",\\"prevent\\", v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with self modifier 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><button block-handler-0=\\"click\\"><span>Button</span></button><button block-handler-1=\\"click.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
const v2 = ctx['onClickSelf'];
let d2 = [\\"self\\", v2, ctx];
return block1([d1, d2]);
}
}"
`;
exports[`t-on t-on modifiers (synthetic listener) basic support for synthetic 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-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['divClicked'];
let d1 = [\\"synthetic\\", v1, ctx];
const v2 = ctx['btnClicked'];
let d2 = [\\"synthetic\\", v2, ctx];
return block1([d1, d2]);
}
}"
`;
exports[`t-on t-on with inline statement (function call) 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let d1 = [()=>v1.incrementCounter(2), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on with inline statement 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(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let d1 = [()=>v1.counter++, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on with inline statement, part 2 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(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let d1 = [()=>v1.flag=!v1.flag, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on with inline statement, part 3 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(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
const v2 = ctx['someFunction'];
let d1 = [()=>v1.n=v2(3), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on with t-call 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(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['update'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on with t-call 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;
const callTemplate_2 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`t-on t-on, with arguments and t-call 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(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['value'];
let d1 = [()=>this.update(v1), ctx];
return block1([d1]);
}
}"
`;
exports[`t-on t-on, with arguments and t-call 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;
const callTemplate_2 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
@@ -0,0 +1,338 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`misc complex template 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-attribute-0=\\"class\\"><div block-attribute-1=\\"class\\"><div class=\\"batch_header\\"><a block-attribute-2=\\"href\\" block-attribute-3=\\"class\\" title=\\"View Batch\\"><block-text-4/><block-child-0/><i class=\\"arrow fa fa-window-maximize\\"/></a></div><block-child-1/><div class=\\"batch_slots\\"><block-child-2/><block-child-3/></div><div class=\\"batch_commits\\"><block-child-4/></div></div></div>\`);
let block2 = createBlock(\`<i class=\\"fa fa-exclamation-triangle\\"/>\`);
let block3 = createBlock(\`<span><i class=\\"fa fa-cog fa-spin fa-fw\\"/> preparing</span>\`);
let block7 = createBlock(\`<div class=\\"slot_filler\\"/>\`);
let block9 = createBlock(\`<div class=\\"one_line\\"><a block-attribute-0=\\"href\\" block-attribute-1=\\"class\\"><block-child-0/><block-child-1/><block-child-2/><block-child-3/><block-text-2/></a><a block-attribute-3=\\"href\\" class=\\"badge badge-light\\" title=\\"View Commit on Github\\"><i class=\\"fa fa-github\\"/></a><span><block-text-4/></span></div>\`);
let block10 = createBlock(\`<i class=\\"fa fa-fw fa-hashtag\\" title=\\"This commit is a new head\\"/>\`);
let block11 = createBlock(\`<i class=\\"fa fa-fw fa-link\\" title=\\"This commit is an existing head from bundle branches\\"/>\`);
let block12 = createBlock(\`<i class=\\"fa fa-fw fa-code-fork\\" title=\\"This commit is matched from a base batch with matching merge_base\\"/>\`);
let block13 = createBlock(\`<i class=\\"fa fa-fw fa-clock-o\\" title=\\"This commit is the head of a base branch\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3,b4,b6,b8;
let d1 = \`batch_tile \${ctx['options'].more?'more':'nomore'}\`;
let d2 = \`card bg-\${ctx['klass']}-light\`;
let d3 = \`/runbot/batch/\${ctx['batch'].id}\`;
let d4 = \`badge badge-\${ctx['batch'].has_warning?'warning':'light'}\`;
let d5 = ctx['batch'].formated_age;
if (ctx['batch'].has_warning) {
b2 = block2();
}
if (ctx['batch'].state=='preparing') {
b3 = block3();
}
ctx = Object.create(ctx);
const [k_block4, v_block4, l_block4, c_block4] = prepareList(ctx['batch'].slot_ids.filter(slot=>slot.build_id.id&&!slot.trigger_id.manual&&(ctx['options'].trigger_display[slot.trigger_id.id])));
for (let i1 = 0; i1 < l_block4; i1++) {
ctx[\`slot\`] = v_block4[i1];
let key1 = ctx['slot'].id;
c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'],slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1);
}
ctx = ctx.__proto__;
b4 = list(c_block4);
ctx = Object.create(ctx);
const [k_block6, v_block6, l_block6, c_block6] = prepareList([1,2,3,4]);
for (let i1 = 0; i1 < l_block6; i1++) {
ctx[\`x\`] = v_block6[i1];
let key1 = ctx['x'];
c_block6[i1] = withKey(block7(), key1);
}
ctx = ctx.__proto__;
b6 = list(c_block6);
ctx = Object.create(ctx);
const [k_block8, v_block8, l_block8, c_block8] = prepareList(ctx['commit_links']);
for (let i1 = 0; i1 < l_block8; i1++) {
ctx[\`commit_link\`] = v_block8[i1];
let key1 = ctx['commit_link'].id;
let b10,b11,b12,b13;
let d6 = \`/runbot/commit/\${ctx['commit_link'].commit_id}\`;
let d7 = \`badge badge-light batch_commit match_type_\${ctx['commit_link'].match_type}\`;
if (ctx['commit_link'].match_type=='new') {
b10 = block10();
}
if (ctx['commit_link'].match_type=='head') {
b11 = block11();
}
if (ctx['commit_link'].match_type=='base_match') {
b12 = block12();
}
if (ctx['commit_link'].match_type=='base_head') {
b13 = block13();
}
let d8 = ctx['commit_link'].commit_dname;
let d9 = 'https://%s/commit/%s'%(ctx['commit_link'].commit_remote_url,ctx['commit_link'].commit_name);
let d10 = ctx['commit_link'].commit_subject;
c_block8[i1] = withKey(block9([d6, d7, d8, d9, d10], [b10, b11, b12, b13]), key1);
}
b8 = list(c_block8);
return block1([d1, d2, d3, d4, d5], [b2, b3, b4, b6, b8]);
}
}"
`;
exports[`misc global 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(\`<año block-attribute-0=\\"falló\\"><block-child-0/></año>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = 'agüero';
let b2 = ctx[zero];
return block1([d1], [b2]);
}
}"
`;
exports[`misc global 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(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = withDefault(ctx['foo'], \`foo default\`);
return block1([d1]);
}
}"
`;
exports[`misc global 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 b3 = text(\`toto default\`);
let b2 = withDefault(safeOutput(ctx['toto']), b3);
return block1([], [b2]);
}
}"
`;
exports[`misc global 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;
const callTemplate_2 = getTemplate(\`_callee-uses-foo\`);
const callTemplate_4 = getTemplate(\`_callee-uses-foo\`);
const callTemplate_6 = getTemplate(\`_callee-uses-foo\`);
const callTemplate_8 = getTemplate(\`_callee-asc\`);
const callTemplate_10 = getTemplate(\`_callee-asc-toto\`);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
let block4 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList([4,5,6]);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`value\`] = v_block2[i1];
ctx[\`value_first\`] = i1 === 0;
ctx[\`value_last\`] = i1 === v_block2.length - 1;
ctx[\`value_index\`] = i1;
ctx[\`value_value\`] = k_block2[i1];
let key1 = ctx['value'];
let d1 = ctx['value'];
let b4 = block4([d1]);
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"foo\\", 'aaa');
let b6 = callTemplate_2.call(this, ctx, node, key + \`__1__\${key1}\`);
ctx = ctx.__proto__;
let b7 = callTemplate_4.call(this, ctx, node, key + \`__3__\${key1}\`);
setContextValue(ctx, \\"foo\\", 'bbb');
let b8 = callTemplate_6.call(this, ctx, node, key + \`__5__\${key1}\`);
let b5 = multi([b6, b7, b8]);
ctx[zero] = b5;
let b9 = callTemplate_8.call(this, ctx, node, key + \`__7__\${key1}\`);
ctx = ctx.__proto__;
c_block2[i1] = withKey(multi([b4, b9]), key1);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let b10 = callTemplate_10.call(this, ctx, node, key + \`__9\`);
return block1([], [b2, b10]);
}
}"
`;
exports[`misc other complex template 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;
const callTemplate_14 = getTemplate(\`LOAD_INFOS_TEMPLATE\`);
let block1 = createBlock(\`<div><header><nav class=\\"navbar navbar-expand-md navbar-light bg-light\\"><a block-attribute-0=\\"href\\"><b style=\\"color:#777;\\"><block-text-1/></b></a><button type=\\"button\\" class=\\"navbar-toggler\\" data-toggle=\\"collapse\\" data-target=\\"#top_menu_collapse\\"><span class=\\"navbar-toggler-icon\\"/></button><div class=\\"collapse navbar-collapse\\" id=\\"top_menu_collapse\\" aria-expanded=\\"false\\"><ul class=\\"nav navbar-nav ml-auto text-right\\" id=\\"top_menu\\"><block-child-0/><li class=\\"nav-item divider\\"/><block-child-1/></ul><div><div class=\\"input-group input-group-sm\\"><div class=\\"input-group-prepend input-group-sm\\"><button class=\\"btn btn-default fa fa-cog\\" title=\\"Settings\\" block-handler-2=\\"click\\"/><button class=\\"btn btn-default\\" block-handler-3=\\"click\\"> More </button><block-child-2/></div><input class=\\"form-control\\" type=\\"text\\" placeholder=\\"Search\\" aria-label=\\"Search\\" name=\\"search\\" block-attribute-4=\\"value\\" block-handler-5=\\"keyup\\" block-handler-6=\\"change\\" block-ref=\\"7\\"/><div class=\\"input-group-append\\"><button class=\\"btn btn-default fa fa-eraser\\" block-handler-8=\\"click\\"/></div></div></div></div></nav></header><div class=\\"container-fluid\\" block-ref=\\"9\\"><div class=\\"row\\"><!--div class=\\"form-group col-md-6\\">
<h5>Search options</h5>
<input class=\\"form-control\\" type=\\"text\\" name=\\"default_search\\" id=\\"default_search\\" t-att-checked=\\"default_search\\" placeholder=\\"Default search\\"/>
<h5>Display options</h5>
<div class=\\"form-check\\">
<input class=\\"form-check-input\\" type=\\"checkbox\\" name=\\"display_sticky\\"/>
<label class=\\"form-check-label\\" for=\\"display_sticky\\">Display sticky</label>
</div>
<div class=\\"form-check\\">
<input class=\\"form-check-input\\" type=\\"checkbox\\" name=\\"display_dev\\"/>
<label class=\\"form-check-label\\" for=\\"display_dev\\">Display dev</label>
</div>
</div--><div class=\\"form-group col-md-6\\"><h5>Triggers</h5><block-child-3/></div></div></div><div class=\\"container-fluid frontend\\"><div class=\\"row\\"><div class=\\"col-md-12\\"><block-child-4/></div><div class=\\"col-md-12\\"><block-child-5/><block-child-6/><block-child-7/></div></div></div></div>\`);
let block3 = createBlock(\`<li class=\\"nav-item\\"><a class=\\"nav-link\\" href=\\"#\\" block-handler-0=\\"click\\"><block-text-1/></a></li>\`);
let block5 = createBlock(\`<li class=\\"nav-item dropdown\\"><b><a class=\\"nav-link\\" block-attribute-0=\\"href\\">Login</a></b></li>\`);
let block8 = createBlock(\`<li class=\\"nav-item\\"><a href=\\"/runbot/errors\\" class=\\"nav-link text-danger\\" block-attribute-0=\\"title\\"><i class=\\"fa fa-bug\\"/><block-text-1/></a></li>\`);
let block9 = createBlock(\`<li class=\\"nav-item divider\\"/>\`);
let block11 = createBlock(\`<li class=\\"nav-item\\"><a href=\\"/runbot/errors\\" class=\\"nav-link\\" title=\\"Random Bugs\\"><i class=\\"fa fa-bug\\"/></a></li>\`);
let block12 = createBlock(\`<li class=\\"nav-item divider\\"/>\`);
let block13 = createBlock(\`<li class=\\"nav-item dropdown\\"><a href=\\"#\\" class=\\"nav-link dropdown-toggle\\" data-toggle=\\"dropdown\\"><b><span><block-text-0/></span></b></a><div class=\\"dropdown-menu js_usermenu\\" role=\\"menu\\"><a class=\\"dropdown-item\\" id=\\"o_logout\\" role=\\"menuitem\\" block-attribute-1=\\"href\\">Logout</a><a class=\\"dropdown-item\\" role=\\"menuitem\\" block-attribute-2=\\"href\\">Web</a></div></li>\`);
let block14 = createBlock(\`<select class=\\"custom-select\\" name=\\"category\\" id=\\"category\\"><block-child-0/></select>\`);
let block16 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
let block20 = createBlock(\`<div class=\\"form-check\\"><input class=\\"form-check-input\\" type=\\"checkbox\\" block-attribute-0=\\"name\\" block-attribute-1=\\"id\\" block-attribute-2=\\"checked\\" block-attribute-3=\\"data-trigger_id\\" block-handler-4=\\"change\\"/><label class=\\"form-check-label\\" block-attribute-5=\\"for\\"><block-text-6/></label></div>\`);
let block21 = createBlock(\`<div><button class=\\"btn btn-sm btn-default\\" block-handler-0=\\"click\\">All</button><button class=\\"btn btn-sm btn-default\\" block-handler-1=\\"click\\">None</button><button class=\\"btn btn-sm btn-info\\" block-handler-2=\\"click\\">Default</button><button class=\\"btn btn-sm btn-default\\" block-handler-3=\\"click\\">Close</button></div>\`);
let block23 = createBlock(\`<div class=\\"alert alert-warning\\" role=\\"alert\\"><block-text-0/> <!-- todo fixme--></div>\`);
let block24 = createBlock(\`<div class=\\"mb32\\"><h1>No project</h1></div>\`);
let block25 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
let b2,b4,b14,b17,b22,b23,b24,b25;
let d1 = \`/runbot/\${ctx['project'].slug}\`;
let d2 = ctx['project'].name;
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['projects']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`project\`] = v_block2[i1];
let key1 = ctx['project'].id;
const v1 = ctx['selectProject'];
const v2 = ctx['project'];
let d3 = [v1(v2), ctx];
let d4 = ctx['project'].name;
c_block2[i1] = withKey(block3([d3, d4]), key1);
}
ctx = ctx.__proto__;
b2 = list(c_block2);
if (ctx['user']) {
let b5,b6;
if (ctx['user'].public) {
let d5 = \`/web/login?redirect=/\`;
b5 = block5([d5]);
} else {
let b7,b10,b13;
if (ctx['nb_assigned_errors']&&ctx['nb_assigned_errors']>0) {
let d6 = \`You have \${ctx['nb_assigned_errors']} random bug assigned\`;
let d7 = ctx['nb_assigned_errors'];
let b8 = block8([d6, d7]);
let b9 = block9();
b7 = multi([b8, b9]);
} else if (ctx['nb_build_errors']&&ctx['nb_build_errors']>0) {
let b11 = block11();
let b12 = block12();
b10 = multi([b11, b12]);
}
let d8 = ctx['user'].name.length>25?ctx['user'].namesubstring(0,23)+'...':ctx['user'].name;
let d9 = \`/web/session/logout?redirect=/\`;
let d10 = \`/web\`;
b13 = block13([d8, d9, d10]);
b6 = multi([b7, b10, b13]);
}
b4 = multi([b5, b6]);
}
const v3 = ctx['toggleSettingsMenu'];
let d11 = [v3, ctx];
const v4 = ctx['toggleMore'];
let d12 = [v4, ctx];
if (ctx['categories']&&ctx['categories'].length>1) {
ctx = Object.create(ctx);
const [k_block15, v_block15, l_block15, c_block15] = prepareList(ctx['categories']);
for (let i1 = 0; i1 < l_block15; i1++) {
ctx[\`category\`] = v_block15[i1];
let key1 = ctx['category'].id;
let d13 = ctx['category'].id;
let d14 = ctx['category'].id==ctx['options'].active_category_id;
let d15 = ctx['category'].name;
c_block15[i1] = withKey(block16([d13, d14, d15]), key1);
}
ctx = ctx.__proto__;
let b15 = list(c_block15);
b14 = block14([], [b15]);
}
let d16 = ctx['search'].value;
const v5 = ctx['updateFilter'];
let d17 = [v5, ctx];
const v6 = ctx['updateFilter'];
let d18 = [v6, ctx];
let d19 = (el) => refs[\`search_input\`] = el;
const v7 = ctx['clearSearch'];
let d20 = [v7, ctx];
let d21 = (el) => refs[\`settings_menu\`] = el;
if (ctx['triggers']) {
ctx = Object.create(ctx);
const [k_block18, v_block18, l_block18, c_block18] = prepareList(ctx['triggers']);
for (let i1 = 0; i1 < l_block18; i1++) {
ctx[\`trigger\`] = v_block18[i1];
let key1 = ctx['trigger'].id;
let b20;
if (!ctx['trigger'].manual&&ctx['trigger'].project_id===ctx['project'].id&&ctx['trigger'].category_id===ctx['options'].active_category_id) {
let d22 = \`trigger_\${ctx['trigger'].id}\`;
let d23 = \`trigger_\${ctx['trigger'].id}\`;
let d24 = ctx['options'].trigger_display[ctx['trigger'].id];
let d25 = ctx['trigger'].id;
const v8 = ctx['updateTriggerDisplay'];
let d26 = [v8, ctx];
let d27 = \`trigger_\${ctx['trigger'].id}\`;
let d28 = ctx['trigger'].name;
b20 = block20([d22, d23, d24, d25, d26, d27, d28]);
}
c_block18[i1] = withKey(multi([b20]), key1);
}
ctx = ctx.__proto__;
let b18 = list(c_block18);
const v9 = ctx['triggerAll'];
let d29 = [v9, ctx];
const v10 = ctx['triggerNone'];
let d30 = [v10, ctx];
const v11 = ctx['triggerDefault'];
let d31 = [v11, ctx];
const v12 = ctx['toggleSettingsMenu'];
let d32 = [v12, ctx];
let b21 = block21([d29, d30, d31, d32]);
b17 = multi([b18, b21]);
}
if (ctx['load_infos']) {
b22 = callTemplate_14.call(this, ctx, node, key + \`__13\`);
}
if (ctx['message']) {
let d33 = ctx['message'];
b23 = block23([d33]);
}
if (!ctx['project']) {
b24 = block24();
} else {
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__15\`, node, ctx);
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__16\`, node, ctx);
b25 = block25([], [b26, b27]);
}
return block1([d1, d2, d11, d12, d16, d17, d18, d19, d20, d21], [b2, b4, b14, b17, b22, b23, b24, b25]);
}
}"
`;
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb parser simple t-foreach expression, t-key mandatory 1`] = `"\\"Directive t-foreach should always be used with a t-key!\\" (expression: t-foreach=\\"list\\" t-as=\\"item\\")"`;
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`memory t-foreach does not leak stuff in global scope 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(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList([3,2,1]);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
ctx[\`item_index\`] = i1;
let key1 = ctx['item_index'];
c_block2[i1] = withKey(text(ctx['item']), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
@@ -0,0 +1,376 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`simple templates, mostly static can render a table row 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(\`<tr><td>cell</td></tr>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with a class attribute 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 class=\\"abc\\">foo</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with a class attribute with a quote 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 class=\\"a'bc\\">word</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with a span child node 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><span>word</span></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with an arbitrary attribute with a quote 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 abc=\\"a'bc\\">word</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with an empty class attribute 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>word</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static div with content 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>foo</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static dom node with t-esc 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/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['text'];
return block1([d1]);
}
}"
`;
exports[`simple templates, mostly static dom node with t-esc 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['text'];
return block1([d1]);
}
}"
`;
exports[`simple templates, mostly static dynamic text value 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;
return function template(ctx, node, key = \\"\\") {
return text(ctx['text']);
}
}"
`;
exports[`simple templates, mostly static empty div 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/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`simple templates, mostly static empty string 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;
return function template(ctx, node, key = \\"\\") {
return text(\`\`);
}
}"
`;
exports[`simple templates, mostly static empty string in a template set 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;
return function template(ctx, node, key = \\"\\") {
return text(\`\`);
}
}"
`;
exports[`simple templates, mostly static inline template string in t-esc 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;
return function template(ctx, node, key = \\"\\") {
return text(\`text\`);
}
}"
`;
exports[`simple templates, mostly static inline template string with content in t-esc 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;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v\\", 1);
return text(\`text\${ctx['v']}\`);
}
}"
`;
exports[`simple templates, mostly static inline template string with variable in 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;
return function template(ctx, node, key = \\"\\") {
return text(\`text \${ctx['v']}\`);
}
}"
`;
exports[`simple templates, mostly static multiple root nodes 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 block2 = createBlock(\`<div>foo</div>\`);
let block3 = createBlock(\`<span>hey</span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = block2();
let b3 = block3();
return multi([b2, b3]);
}
}"
`;
exports[`simple templates, mostly static simple string 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;
return function template(ctx, node, key = \\"\\") {
return text(\`hello vdom\`);
}
}"
`;
exports[`simple templates, mostly static simple string in t tag 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;
return function template(ctx, node, key = \\"\\") {
return text(\`hello vdom\`);
}
}"
`;
exports[`simple templates, mostly static static text and dynamic text (no t tag) 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;
return function template(ctx, node, key = \\"\\") {
let b2 = text(\`hello \`);
let b3 = text(ctx['text']);
return multi([b2, b3]);
}
}"
`;
exports[`simple templates, mostly static static text and dynamic text 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;
return function template(ctx, node, key = \\"\\") {
let b2 = text(\`hello \`);
let b3 = text(ctx['text']);
return multi([b2, b3]);
}
}"
`;
exports[`simple templates, mostly static t-esc in dom node 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/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['text'];
return block1([d1]);
}
}"
`;
exports[`simple templates, mostly static t-esc in dom node, variations 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>hello <block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['text'];
return block1([d1]);
}
}"
`;
exports[`simple templates, mostly static t-esc in dom node, variations 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>hello <block-text-0/> world</div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['text'];
return block1([d1]);
}
}"
`;
exports[`simple templates, mostly static two t-escs next to each other 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;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['text1']);
let b3 = text(ctx['text2']);
return multi([b2, b3]);
}
}"
`;
exports[`simple templates, mostly static two t-escs next to each other 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;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['text1']);
let b3 = text(ctx['text2']);
return multi([b2, b3]);
}
}"
`;
exports[`simple templates, mostly static two t-escs next to each other, in a div 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['text1'];
let d2 = ctx['text2'];
return block1([d1, d2]);
}
}"
`;
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properly support svg add proper namespace to g tags 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(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg add proper namespace to svg 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(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to g tags not added if already in svg namespace 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(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to svg tags added even if already in svg namespace 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(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg block-ns=\\"http://www.w3.org/2000/svg\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`debugging t-debug 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-child-0/></div>\`);
let block2 = createBlock(\`<span>hey</span>\`);
return function template(ctx, node, key = \\"\\") {
debugger;
let b2;
if (true) {
debugger;
b2 = block2();
}
return block1([], [b2]);
}
}"
`;
exports[`debugging t-debug on sub template 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(\`<p>coucou</p>\`);
return function template(ctx, node, key = \\"\\") {
debugger;
return block1();
}
}"
`;
exports[`debugging t-debug on sub template 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;
const callTemplate_2 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`debugging t-log 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/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"foo\\", 42);
console.log(ctx['foo']+3);
return block1();
}
}"
`;

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