mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8f7274977 | |||
| 924dffeb36 | |||
| 0dbc807e01 | |||
| 4b88787a72 | |||
| 6f0a4b1c92 | |||
| 3f575148b6 | |||
| 4a971f2963 | |||
| d7850aaf7a | |||
| 0e13b859d0 | |||
| c813a1ce03 | |||
| 54d69c2dde | |||
| 11d4aae8d2 | |||
| b159a073d9 | |||
| d9b189bcba | |||
| 7ef1fe0b99 | |||
| 211ecdf689 | |||
| b208894d38 | |||
| 0737bb39b4 | |||
| de584b01a8 | |||
| 525029b682 | |||
| b88f7a03e9 | |||
| 055cbda750 | |||
| 45f6a8e97a | |||
| 6091912c54 | |||
| 586f90f6d1 | |||
| e62a0ca2d9 | |||
| 3a4ee160ed | |||
| 5f24caf40d | |||
| 8e36a11b6e | |||
| 9a54afcdf5 | |||
| 3eefa4b153 | |||
| 2ffb94ed98 | |||
| 6a15874d23 | |||
| 07d34248ae | |||
| dfc090bb12 | |||
| 7b3593ff8f | |||
| b06791c950 | |||
| f7843c7c00 | |||
| ca9d4ea762 | |||
| 7a407a762d | |||
| 8847824fb7 | |||
| 55b068faeb | |||
| dbee8f50d1 | |||
| f5fec02d4c | |||
| 651c53ebbf | |||
| 3119497508 | |||
| 057e6944d6 | |||
| 83d38a6f48 | |||
| 9577d70b4b | |||
| 6ff6d59895 | |||
| 7ba0c9f8ba | |||
| 79a51f2317 | |||
| 483335df6c | |||
| 287a6d5ac4 | |||
| 68684a9a45 | |||
| e5d773daa8 | |||
| 00ffab1f64 | |||
| a226f92def | |||
| 1749bce155 | |||
| c00127fa1e | |||
| 0b4b7899f6 | |||
| 5c324fa075 | |||
| 3945c009ed | |||
| 5b1132f01a | |||
| f014b1a33e | |||
| 3f09a953b0 | |||
| b8e7ee5ff7 | |||
| 7fdca35a74 | |||
| 489e20843c | |||
| 34b781aeed | |||
| e8b0f31da6 | |||
| d160c4a628 | |||
| 4542171a31 | |||
| 0b1c4dd4ef | |||
| f4da50d350 | |||
| f2921abda8 | |||
| 107200fd94 | |||
| 0f8c859d5f | |||
| cca8438d38 | |||
| a727347d60 | |||
| f19de73b0f | |||
| a8ffcafd23 | |||
| a2f01ef4ad | |||
| d7de25e867 | |||
| 424bc480e2 | |||
| 421f400766 | |||
| c37f3445f6 | |||
| 4cde06d685 | |||
| 7b95315b87 | |||
| 09761ddd8e | |||
| b3c6ec2b48 | |||
| b20462ada6 | |||
| 25000b36be | |||
| 144d4a253c | |||
| 9d48bda227 | |||
| c8db663869 | |||
| 247200194f | |||
| 60b8817ac0 | |||
| e008966291 | |||
| e2fedc6eff | |||
| b3fb9a35bf | |||
| 240259568e | |||
| 18fb67eaad | |||
| e946967867 | |||
| 777b2aae5e | |||
| 0831bb54e3 | |||
| 03f34a38dc | |||
| dcefd26bee | |||
| 922dab8e8f | |||
| 99740f9993 | |||
| 0b79b1c8fd | |||
| 5b0dce94cf | |||
| a1d435c5a5 | |||
| 13c3178760 | |||
| 2b565ce1b4 | |||
| cfcf2c6714 | |||
| 92174559a1 | |||
| 840348892f | |||
| b988a68b6f | |||
| 7869a1f2c6 | |||
| 9e81d14d50 | |||
| 0e4a55ba09 | |||
| 6e9b68dafa | |||
| ba7c9063c0 | |||
| 3a361876ad | |||
| 58b8572f0a | |||
| c23637e6d8 | |||
| 4a4b1fbba5 | |||
| 536d9e1762 | |||
| 6ed68372a6 | |||
| efd934d2b1 | |||
| dabc24cee3 | |||
| 7f49796a07 | |||
| 757dffefac | |||
| bdfe058279 | |||
| 345c44b952 | |||
| ea74739d46 | |||
| 93b53d8017 | |||
| c2284bc6f5 | |||
| 48650da62e | |||
| 2edd6cb8f3 | |||
| 4b0a37c542 | |||
| 50c0e30936 | |||
| e97335d7ab | |||
| 6ea40a66da | |||
| 2806cda420 | |||
| cb0b525f32 | |||
| 30d28670e8 | |||
| 7b761a2f8b | |||
| d834bb2ac4 | |||
| 6ae92fa4b3 | |||
| 3ceb118ace | |||
| fa426b7fc0 | |||
| d7403871fc | |||
| 6aeff21d9a | |||
| 429e145b68 | |||
| fb6aab8b70 | |||
| 9e285e1e0a | |||
| 63bfdfb8db | |||
| d4c4fe853e | |||
| 9163bb1e08 | |||
| a187a376a0 | |||
| 06ea6d2490 | |||
| 04334e23d7 | |||
| 4efe1ae166 | |||
| ad4673cb36 | |||
| e611c20ab4 | |||
| d60a5a414e | |||
| d47dcf6be2 | |||
| 3e70b17da1 | |||
| c718d8e6c6 | |||
| a122a94180 | |||
| ca139166ab | |||
| ae71db28e4 | |||
| 779fc6b02b | |||
| 84913593b2 | |||
| 8d4eb21536 | |||
| 4bdfaf96f3 | |||
| 233c953243 | |||
| ee5ad354ff | |||
| 4e4b85e2a9 | |||
| 6479631983 | |||
| 2a0c410014 | |||
| bb4aecf638 | |||
| a1adfd5a1b | |||
| dd4848f602 | |||
| b25f476c22 | |||
| 90cdb97b49 | |||
| 7d7568d254 | |||
| 06d207f60e | |||
| ff564421da | |||
| c3695ec8db | |||
| 6e0834ce5e | |||
| d1425c7100 | |||
| e01fe301c0 | |||
| ba2fe3ff55 | |||
| 700d574314 | |||
| ba32772a92 | |||
| e4a0277f68 | |||
| abe3748825 | |||
| ee64de8b3f | |||
| ed831320db | |||
| 5f1e1e189f | |||
| 55ef9ca116 | |||
| 642ecf0ccd | |||
| c9cc789f1d | |||
| b8649f1add | |||
| e2d98e4c26 | |||
| e6bb4ef286 | |||
| 7ac20f4fc2 |
@@ -5,7 +5,7 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ master, owl-next ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
+33
-45
@@ -30,33 +30,19 @@ All changes are documented here in no particular order.
|
||||
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
|
||||
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
|
||||
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
|
||||
|
||||
|
||||
|
||||
**Templates**
|
||||
|
||||
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
|
||||
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
|
||||
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
||||
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
|
||||
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
|
||||
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
||||
- new: the `this` variable in template expressions is now bound to the component
|
||||
|
||||
|
||||
**Reactivity**
|
||||
|
||||
- finer grained reactivity: owl 2 tracks change per key/component
|
||||
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
|
||||
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
|
||||
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
|
||||
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
|
||||
|
||||
|
||||
**Slots**
|
||||
@@ -80,7 +66,10 @@ All changes are documented here in no particular order.
|
||||
|
||||
- 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) ([doc](doc/reference/app.md))
|
||||
- new `Memo` component
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||
@@ -248,12 +237,35 @@ 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, so the same rationale applies: in a way,
|
||||
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
||||
every value used by the component.
|
||||
a much more powerful reactivity system.
|
||||
|
||||
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||
as before.
|
||||
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 is removed
|
||||
|
||||
@@ -775,28 +787,4 @@ Rationale: the `browser` object caused more trouble than it was worth. Also, it
|
||||
seems like this should be done in user space, not at the framework level.
|
||||
|
||||
Migration: code should just be adapted to either use another browser object,
|
||||
or to use native browser function (and then, just mock them directly).
|
||||
|
||||
## 40. Rendering a component does not necessarily render child components
|
||||
|
||||
Before, if one had the following component tree:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
```
|
||||
|
||||
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
|
||||
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
|
||||
if their props have changed.
|
||||
|
||||
Now, the question is what happens if the props have changed, but in a deeper way?
|
||||
In that case, Owl will know, because each props are now reactive. So, if some
|
||||
inner value read by `B` was changed, then only `B` will be updated.
|
||||
|
||||
Rationale: This was just not possible in Owl 1, but it now possible. This is
|
||||
due to the rewriteof the underlying rendering engine and the reactivity
|
||||
system. The goal is to have a big performance boost in large screen with many
|
||||
components: now Owl only rerender what is strictly useful.
|
||||
|
||||
or to use native browser function (and then, just mock them directly).
|
||||
@@ -110,7 +110,6 @@ Are you new to Owl? This is the place to start!
|
||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||
|
||||
## Installing Owl
|
||||
|
||||
@@ -122,5 +121,5 @@ npm install @odoo/owl
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-1.4.10](https://github.com/odoo/owl/releases/tag/v1.4.10)
|
||||
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# 🦉 Notes On Owl Compiled Templates 🦉
|
||||
|
||||
This page will explain what an Owl compiled template look like. This is a
|
||||
technical document intended for developers interested in understanding how Owl
|
||||
works internally.
|
||||
|
||||
Broadly speaking, Owl compiles templates into a javascript function (a closure)
|
||||
that returns a function (the "render" function). The point of the closure is to
|
||||
have a place to store all values specific to the template (in particular, "blocks").
|
||||
Once a template is compiled, its closure function is called once to get the
|
||||
render function, and from then on, only the render function is used.
|
||||
|
||||
The render function takes some context (and some additional information) and
|
||||
return a virtual dom representation of the rendered template, as a block tree.
|
||||
A block tree is a very light weight representation that only contains the dynamic
|
||||
part of the template, and its structure. It is actually independant of the
|
||||
static part of the templates (which are contained in the blocks captured by the
|
||||
closure). This means that the work performed at render time is only to collect
|
||||
dynamic data, and to describe the block structure of the result.
|
||||
|
||||
It looks like this, in pseudo code:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
// here is some place to put stuff specific to the template, such as
|
||||
// blocks
|
||||
...
|
||||
|
||||
return function render(context, node, key) {
|
||||
// only build here all dynamic parts of the template
|
||||
// build a block tree
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let us see an example. Consider the following template:
|
||||
|
||||
```xml
|
||||
<div class="some-class">
|
||||
<div class="blabla">
|
||||
<span><t t-esc="state.value"/></span>
|
||||
</div>
|
||||
<t t-if="state.info">
|
||||
<p class="info" t-att-class="someAttribute">
|
||||
<t t-esc="state.info"/>
|
||||
</p>
|
||||
</t>
|
||||
<SomeComponent value="value"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
If you look carefully, there are 5 dynamic things:
|
||||
|
||||
- a text value (the first `t-esc`),
|
||||
- a sub block (the `t-if`),
|
||||
- a dynamic attribute (the `t-att-class` attribute),
|
||||
- another text value (the second `t-esc`),
|
||||
- and finally, a sub component
|
||||
|
||||
Here is the compiled code for this template:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(
|
||||
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
|
||||
);
|
||||
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
|
||||
|
||||
return function render(ctx, node, key = "") {
|
||||
let b2, b3;
|
||||
let txt1 = ctx["state"].value;
|
||||
if (ctx["state"].info) {
|
||||
let attr1 = ctx["someAttribute"];
|
||||
let txt2 = ctx["state"].info;
|
||||
b2 = block2([attr1, txt2]);
|
||||
}
|
||||
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
|
||||
return block1([txt1], [b2, b3]);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The values captured in the closure capture the static part of the template: we
|
||||
define here two blocks (which contains a template node, that can be deep cloned
|
||||
whenever a block is mounted). Then the render function only describes the block
|
||||
tree structure of the result, depending on the context. This means that we
|
||||
minimize the amount of work done at render time.
|
||||
|
||||
Then, when we want to patch the dom, Owl will uses the `patch` function from
|
||||
blockdom, which then will diff the block tree, and deep clone new blocks whenever
|
||||
a new block is inserted, keep track of dynamic parts of each block, and update
|
||||
them accordingly.
|
||||
|
||||
With this design, the cost of rendering a template is proportional to the number
|
||||
of dynamic values, and not to the size of the template.
|
||||
+16
-21
@@ -3,7 +3,6 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Loading templates](#loading-templates)
|
||||
@@ -27,10 +26,24 @@ The basic workflow is: create an `App` instance configured with the root
|
||||
component, the templates, and possibly other settings. Then, we mount that
|
||||
instance somewhere in the DOM.
|
||||
|
||||
## API
|
||||
## Configuration
|
||||
|
||||
- **`constructor(Root[, config])`**: first argument should be a component class (not
|
||||
an instance), and the optional second argument is a configuration object (see below).
|
||||
an instance), and the optional second argument is a configuration object.
|
||||
|
||||
The `config` object is an object with some of the following keys:
|
||||
|
||||
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||
- **`props (object)`**: the props given to the root component
|
||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
|
||||
mode, which activates some additional checks (in particular, the props validation
|
||||
code is only performed in dev mode)
|
||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||
be translated (see [translations](translations.md))
|
||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
|
||||
- **`mount(target, options)`**: first argument is an html element, and the optional
|
||||
second argument is an object with mounting options (see below). Mount the app
|
||||
@@ -45,24 +58,6 @@ instance somewhere in the DOM.
|
||||
|
||||
- **`destroy()`**: destroys the application
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config` object is an object with some of the following keys:
|
||||
|
||||
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||
- **`props (object)`**: the props given to the root component
|
||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
|
||||
mode, which activates some additional checks (in particular, the props validation
|
||||
code is only performed in dev mode)
|
||||
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
||||
that Owll will not log a message to warn that Owl is in `dev` mode.
|
||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||
be translated (see [translations](translations.md))
|
||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
|
||||
## `mount` helper
|
||||
|
||||
Note that there is a `mount` helper to do that in just a line:
|
||||
|
||||
@@ -68,16 +68,12 @@ The `Component` class has a very small API.
|
||||
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||
`willPatch` and `patched`.
|
||||
|
||||
* **`render(deep[=false])`**: calling this method directly will cause a rerender. Note
|
||||
* **`render()`**: calling this method directly will cause a rerender. Note
|
||||
that with the reactivity system, this should be rare to have to do it manually.
|
||||
Also, the rendering operation is asynchronous, so the DOM will only be updated
|
||||
slightly later (at the next animation frame, if no component delays the
|
||||
rendering)
|
||||
|
||||
By default, the render initiated by this method will stop at each child
|
||||
component if their props are (shallow) equal. To force a render to update
|
||||
all child components, one can use the optional `deep` argument.
|
||||
|
||||
## Static Properties
|
||||
|
||||
- **`template (string)`**: this is the name of the template that
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.core.EventBus();
|
||||
|
||||
bus.on("some-event", null, function (...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
@@ -168,15 +168,11 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
|
||||
There is a special `*` prop that means that additional prop are allowed. This is
|
||||
sometimes useful for generic components that will propagate some or all their
|
||||
props to their child components.
|
||||
|
||||
Note that default values cannot be defined for a mandatory props. Doing so will
|
||||
result in a prop validation error.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
@@ -194,8 +190,7 @@ class ComponentB extends owl.Component {
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
optionalProp: { type: Number, optional: true }
|
||||
combinedVal: [Number, Boolean]
|
||||
};
|
||||
|
||||
...
|
||||
|
||||
@@ -25,11 +25,6 @@ To solve this issue, Owl provides two reactivity primitives:
|
||||
|
||||
Most of the time, the `useState` hook is the best solution.
|
||||
|
||||
Since version 2.0, Owl applies the fine grained reactivity at the component
|
||||
level: props are automatically turned into reactive object, so Owl can track
|
||||
which part of these props are consumed by each component, and is therefore able
|
||||
to only rerender the impacted components.
|
||||
|
||||
## `useState`
|
||||
|
||||
Let us start by an example of how `useState` could be used:
|
||||
@@ -122,10 +117,3 @@ rawState.value = 3; // will NOT be picked up by the reactivity system!!!
|
||||
Here again, this is useful in some situations where we want to explicitely bypass
|
||||
Owl, but using this function means that the responsability of coordinating
|
||||
state update is given to the user code, instead of Owl. Subtle bugs may arise!
|
||||
|
||||
Also, normal (non-reactive objects) will be directly returned by `toRaw`:
|
||||
|
||||
```js
|
||||
const obj = { a: 1 };
|
||||
console.log(toRaw(obj) === obj); // true
|
||||
```
|
||||
|
||||
+10
-26
@@ -87,8 +87,8 @@ grandparent of the slot content).
|
||||
|
||||
## Default Slot
|
||||
|
||||
All elements inside the component which are not a named slot will be treated as
|
||||
part of the content of the `default` slot. For example:
|
||||
The first element inside the component which is not a named slot will
|
||||
be considered the `default` slot. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
@@ -110,7 +110,6 @@ One can mix default slot and named slots:
|
||||
default content
|
||||
<t t-set-slot="footer">
|
||||
content for footer slot here
|
||||
</t>
|
||||
</Child>
|
||||
</div>
|
||||
```
|
||||
@@ -210,45 +209,30 @@ use this `Notebook` component:
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
For other kinds of advanced use cases, the content of a slot may depends on some
|
||||
information specific to the generic component. This is the opposite of the slot
|
||||
params.
|
||||
For other kind of advanced use cases, the content of a slot may depends on some
|
||||
specific information specific to the generic component. This is the opposite
|
||||
of the slot params.
|
||||
|
||||
To solve this kind of problems, one can use the `t-slot-scope` directive along
|
||||
with the `t-set-slot`. This defines the name of a variable that can access
|
||||
everything given by the child component:
|
||||
|
||||
```xml
|
||||
<MyComponent>
|
||||
<div>
|
||||
<t t-set-slot="foo" t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</t>
|
||||
</MyComponent>
|
||||
</div>
|
||||
```
|
||||
|
||||
And the child component that includes the slot can provide values like this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" bool="other_var" num="5">
|
||||
<div>
|
||||
<t t-slot="foo" bool="other_var" num="5">
|
||||
</div>
|
||||
```
|
||||
|
||||
In the case of the default slot, you may declare the slot scope directly on the
|
||||
component itself:
|
||||
|
||||
```xml
|
||||
<MyComponent t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
Slot values works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
- [Fragments](#fragments)
|
||||
- [Inline templates](#inline-templates)
|
||||
- [Rendering svg](#rendering-svg)
|
||||
- [Restrictions](#restrictions)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -681,13 +680,3 @@ if a template is supposed to be included in a svg namespace or not. Therefore,
|
||||
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
|
||||
be considered as svg. In practice, this means that each component or each sub
|
||||
templates (included with `t-call`) should have one of these tag as root tag.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Note that Owl templates forbid the use of tag and or attributes starting with
|
||||
the `block-` string. This restriction prevents name collision with the internal
|
||||
code of Owl.
|
||||
|
||||
```xml
|
||||
<div><block-1>this will not be accepted by Owl</block-1></div>
|
||||
```
|
||||
|
||||
@@ -54,17 +54,6 @@ For example, one may want to also translate `data-title` attributes. To do that,
|
||||
we can define additional attributes with the `translatableAttributes` option:
|
||||
|
||||
```js
|
||||
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
|
||||
const app = new App(Root, { templates, tranaslateFn, translatalbeAttributes: ["data-title"] });
|
||||
// ...
|
||||
```
|
||||
|
||||
It is also possible to remove an attribute from the default list by prefixing it with `-`:
|
||||
|
||||
```js
|
||||
const app = new App(Root, {
|
||||
templates,
|
||||
tranaslateFn,
|
||||
translatableAttributes: ["data-title", "-title"],
|
||||
});
|
||||
// data-title attribute will be translated, but not title attribute...
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.0-beta.1",
|
||||
"version": "2.0.0-alpha1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"browser": "dist/owl.iife.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 🦉 OWL Roadmap 🦉
|
||||
|
||||
- Current version: 1.4.10
|
||||
- Current version: 1.4.7
|
||||
- Status: stable
|
||||
|
||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||
|
||||
+16
-22
@@ -4,7 +4,6 @@ import { MountOptions } from "../component/fibers";
|
||||
import { Scheduler } from "../component/scheduler";
|
||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||
import { nodeErrorHandlers } from "../component/error_handling";
|
||||
import { validateTarget } from "../utils";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
@@ -15,25 +14,18 @@ export interface Env {
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||
props?: P;
|
||||
env?: E;
|
||||
test?: boolean;
|
||||
}
|
||||
|
||||
export const DEV_MSG = () => {
|
||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
||||
|
||||
return `Owl is running in 'dev' mode.
|
||||
export const DEV_MSG = `Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||
};
|
||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
||||
|
||||
export class App<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
P extends object = any,
|
||||
P = any,
|
||||
E = any
|
||||
> extends TemplateSet {
|
||||
static validateTarget = validateTarget;
|
||||
|
||||
Root: ComponentConstructor<P, E>;
|
||||
props: P;
|
||||
env: E;
|
||||
@@ -43,11 +35,8 @@ export class App<
|
||||
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
||||
super(config);
|
||||
this.Root = Root;
|
||||
if (config.test) {
|
||||
this.dev = true;
|
||||
}
|
||||
if (this.dev && !config.test) {
|
||||
console.info(DEV_MSG());
|
||||
if (config.dev) {
|
||||
console.info(DEV_MSG);
|
||||
}
|
||||
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
||||
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
|
||||
@@ -55,13 +44,22 @@ export class App<
|
||||
}
|
||||
|
||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
this.checkTarget(target);
|
||||
const node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
}
|
||||
|
||||
checkTarget(target: HTMLElement) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||
}
|
||||
if (!document.body.contains(target)) {
|
||||
throw new Error("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
}
|
||||
|
||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||
return new ComponentNode(Component, props, this);
|
||||
}
|
||||
@@ -102,11 +100,7 @@ export class App<
|
||||
}
|
||||
}
|
||||
|
||||
export async function mount<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
P extends object = any,
|
||||
E = any
|
||||
>(
|
||||
export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>(
|
||||
C: T & ComponentConstructor<P, E>,
|
||||
target: HTMLElement,
|
||||
config: AppConfig<P, E> & MountOptions = {}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { BDom, multi, text, toggler } from "../blockdom";
|
||||
import { validateProps } from "../component/props_validation";
|
||||
import { Markup } from "../utils";
|
||||
import { html } from "../blockdom/index";
|
||||
import { TARGET } from "../reactivity";
|
||||
|
||||
/**
|
||||
* This file contains utility functions that will be injected in each template,
|
||||
* to perform various useful tasks in the compiled code.
|
||||
@@ -23,7 +21,7 @@ function callSlot(
|
||||
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
||||
): BDom {
|
||||
key = key + "__slot_" + name;
|
||||
const slots = ctx.props[TARGET].slots || {};
|
||||
const slots = (ctx.props && ctx.props.slots) || {};
|
||||
const { __render, __ctx, __scope } = slots[name] || {};
|
||||
const slotScope = Object.create(__ctx || {});
|
||||
if (__scope) {
|
||||
@@ -137,7 +135,7 @@ export function safeOutput(value: any): ReturnType<typeof toggler> {
|
||||
} else if (value instanceof LazyValue) {
|
||||
safeKey = `lazy_value`;
|
||||
block = value.evaluate();
|
||||
} else if (value instanceof String || typeof value === "string") {
|
||||
} else if (typeof value === "string") {
|
||||
safeKey = "string_unsafe";
|
||||
block = text(value);
|
||||
} else {
|
||||
@@ -183,7 +181,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
||||
};
|
||||
}
|
||||
|
||||
export const helpers = {
|
||||
export const UTILS = {
|
||||
withDefault,
|
||||
zero: Symbol("zero"),
|
||||
isBoundary,
|
||||
|
||||
+25
-30
@@ -1,13 +1,12 @@
|
||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
||||
import { compile, Template } from "../compiler";
|
||||
import { markRaw } from "../reactivity";
|
||||
import { Portal } from "../portal";
|
||||
import { component, getCurrent } from "../component/component_node";
|
||||
import { helpers } from "./template_helpers";
|
||||
import { globalTemplates } from "../utils";
|
||||
import { component } from "../component/component_node";
|
||||
import { UTILS } from "./template_helpers";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
||||
|
||||
export const globalTemplates: { [key: string]: string | Element } = {};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
@@ -38,22 +37,6 @@ function parseXML(xml: string): Document {
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the helpers object that will be injected in each template closure
|
||||
* function
|
||||
*/
|
||||
function makeHelpers(getTemplate: (name: string) => Template): any {
|
||||
return Object.assign({}, helpers, {
|
||||
Portal,
|
||||
markRaw,
|
||||
getTemplate,
|
||||
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
|
||||
const template = getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface TemplateSetConfig {
|
||||
dev?: boolean;
|
||||
translatableAttributes?: string[];
|
||||
@@ -67,7 +50,13 @@ export class TemplateSet {
|
||||
templates: { [name: string]: Template } = {};
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
helpers: any;
|
||||
utils: typeof UTILS = Object.assign({}, UTILS, {
|
||||
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
},
|
||||
getTemplate: (name: string) => this.getTemplate(name),
|
||||
});
|
||||
|
||||
constructor(config: TemplateSetConfig = {}) {
|
||||
this.dev = config.dev || false;
|
||||
@@ -76,7 +65,6 @@ export class TemplateSet {
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
this.helpers = makeHelpers(this.getTemplate.bind(this));
|
||||
}
|
||||
|
||||
addTemplate(
|
||||
@@ -106,12 +94,7 @@ export class TemplateSet {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
const componentName = getCurrent().component.constructor.name;
|
||||
extraInfo = ` (for component "${componentName}")`;
|
||||
} catch {}
|
||||
throw new Error(`Missing template: "${name}"${extraInfo}`);
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
const templateFn = this._compileTemplate(name, rawTemplate);
|
||||
// first add a function to lazily get the template, in case there is a
|
||||
@@ -120,7 +103,7 @@ export class TemplateSet {
|
||||
this.templates[name] = function (context, parent) {
|
||||
return templates[name].call(this, context, parent);
|
||||
};
|
||||
const template = templateFn(bdom, this.helpers);
|
||||
const template = templateFn(bdom, this.utils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
@@ -135,3 +118,15 @@ export class TemplateSet {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// xml tag helper
|
||||
// -----------------------------------------------------------------------------
|
||||
export function xml(...args: Parameters<typeof String.raw>) {
|
||||
const name = `__template__${xml.nextId++}`;
|
||||
const value = String.raw(...args);
|
||||
globalTemplates[name] = value;
|
||||
return name;
|
||||
}
|
||||
|
||||
xml.nextId = 1;
|
||||
|
||||
@@ -139,7 +139,7 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
|
||||
|
||||
export function makePropSetter(name: string): Setter<HTMLElement> {
|
||||
return function setProp(this: HTMLElement, value: any) {
|
||||
(this as any)[name] = value || "";
|
||||
(this as any)[name] = value;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -299,13 +299,12 @@ interface BlockCtx {
|
||||
locations: IndexedLocation[];
|
||||
children: Child[];
|
||||
cbRefs: number[];
|
||||
refList: (() => void)[][];
|
||||
}
|
||||
|
||||
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
|
||||
if (!ctx) {
|
||||
const children = new Array(tree.info.filter((v) => v.type === "child").length);
|
||||
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };
|
||||
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
|
||||
fromIdx = 0;
|
||||
}
|
||||
if (tree.refN) {
|
||||
@@ -409,11 +408,11 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
|
||||
break;
|
||||
}
|
||||
case "ref":
|
||||
const index = ctx.cbRefs.push(info.idx) - 1;
|
||||
ctx.cbRefs.push(info.idx);
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: makeRefSetter(index, ctx.refList),
|
||||
setData: setRef,
|
||||
updateData: NO_OP,
|
||||
});
|
||||
}
|
||||
@@ -427,21 +426,12 @@ function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
|
||||
let B = createBlockClass(template, ctx);
|
||||
|
||||
if (ctx.cbRefs.length) {
|
||||
const cbRefs = ctx.cbRefs;
|
||||
const refList = ctx.refList;
|
||||
let cbRefsNumber = cbRefs.length;
|
||||
const refs = ctx.cbRefs;
|
||||
B = class extends B {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
refList.push(new Array(cbRefsNumber));
|
||||
super.mount(parent, afterNode);
|
||||
for (let cbRef of refList.pop()!) {
|
||||
cbRef();
|
||||
}
|
||||
}
|
||||
remove() {
|
||||
super.remove();
|
||||
for (let cbRef of cbRefs) {
|
||||
let fn = (this as any).data[cbRef];
|
||||
for (let ref of refs) {
|
||||
let fn = (this as any).data[ref];
|
||||
fn(null);
|
||||
}
|
||||
}
|
||||
@@ -486,12 +476,12 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const elementRemove = elementProto.remove;
|
||||
|
||||
class Block {
|
||||
return class Block {
|
||||
el: HTMLElement | undefined;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
data: any[] | undefined;
|
||||
children?: (VNode | undefined)[];
|
||||
refs: Node[] | undefined;
|
||||
data: any[] | undefined;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
children?: (VNode | undefined)[];
|
||||
|
||||
constructor(data?: any[]) {
|
||||
this.data = data;
|
||||
@@ -512,62 +502,46 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
}
|
||||
|
||||
toString() {
|
||||
const div = document.createElement("div");
|
||||
this.mount(div, null);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const el = nodeCloneNode.call(template, true) as HTMLElement;
|
||||
nodeInsertBefore.call(parent, el, afterNode);
|
||||
this.el = el;
|
||||
this.parentEl = parent;
|
||||
}
|
||||
patch(other: Block, withBeforeRemove: boolean) {}
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const el = nodeCloneNode.call(template, true);
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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;
|
||||
};
|
||||
|
||||
Block.prototype.patch = function patch(other: Block, withBeforeRemove: boolean) {
|
||||
}
|
||||
patch(other: Block, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
@@ -612,17 +586,19 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return Block;
|
||||
}
|
||||
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 makeRefSetter(index: number, refs: (() => void)[][]): Setter<HTMLElement> {
|
||||
return function setRef(this: HTMLElement, fn: any) {
|
||||
refs[refs.length - 1][index] = () => fn(this);
|
||||
};
|
||||
function setRef(this: HTMLElement, fn: any) {
|
||||
fn(this);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
|
||||
abstract class VSimpleNode {
|
||||
text: string | String;
|
||||
text: string;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
el?: any;
|
||||
|
||||
constructor(text: string | String) {
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class VComment extends VSimpleNode {
|
||||
patch() {}
|
||||
}
|
||||
|
||||
export function text(str: string | String): VNode<VText> {
|
||||
export function text(str: string): VNode<VText> {
|
||||
return new VText(str);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,24 +214,14 @@ export class CodeGenerator {
|
||||
templateName?: string;
|
||||
dev: boolean;
|
||||
translateFn: (s: string) => string;
|
||||
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
||||
translatableAttributes: string[];
|
||||
ast: AST;
|
||||
staticCalls: { id: string; template: string }[] = [];
|
||||
helpers: Set<string> = new Set();
|
||||
|
||||
constructor(ast: AST, options: CodeGenOptions) {
|
||||
this.translateFn = options.translateFn || ((s: string) => s);
|
||||
if (options.translatableAttributes) {
|
||||
const attrs = new Set(TRANSLATABLE_ATTRS);
|
||||
for (let attr of options.translatableAttributes) {
|
||||
if (attr.startsWith("-")) {
|
||||
attrs.delete(attr.slice(1));
|
||||
} else {
|
||||
attrs.add(attr);
|
||||
}
|
||||
}
|
||||
this.translatableAttributes = [...attrs];
|
||||
}
|
||||
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
|
||||
this.hasSafeContext = options.hasSafeContext || false;
|
||||
this.dev = options.dev || false;
|
||||
this.ast = ast;
|
||||
@@ -1050,50 +1040,29 @@ export class CodeGenerator {
|
||||
return parts.join("__");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a prop name and value into a string suitable to be inserted in the
|
||||
* generated code. For example:
|
||||
*
|
||||
* Name Value Result
|
||||
* ---------------------------------------------------------
|
||||
* "number" "state" "number: ctx['state']"
|
||||
* "something" "" "something: undefined"
|
||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name: string, value: string): string {
|
||||
value = this.captureExpression(value);
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
if (suffix === "bind") {
|
||||
this.helpers.add("bind");
|
||||
name = _name;
|
||||
value = `bind(ctx, ${value || undefined})`;
|
||||
} else {
|
||||
throw new Error("Invalid prop suffix");
|
||||
}
|
||||
}
|
||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||
return `${name}: ${value || undefined}`;
|
||||
}
|
||||
|
||||
formatPropObject(obj: { [prop: string]: any }): string {
|
||||
const params = [];
|
||||
for (const [n, v] of Object.entries(obj)) {
|
||||
params.push(this.formatProp(n, v));
|
||||
}
|
||||
return params.join(", ");
|
||||
}
|
||||
|
||||
compileComponent(ast: ASTComponent, ctx: Context) {
|
||||
let { block } = ctx;
|
||||
|
||||
// props
|
||||
const hasSlotsProp = "slots" in ast.props;
|
||||
const props: string[] = [];
|
||||
const propExpr = this.formatPropObject(ast.props);
|
||||
if (propExpr) {
|
||||
props.push(propExpr);
|
||||
let hasSlotsProp = false;
|
||||
for (let propName in ast.props) {
|
||||
let propValue = this.captureExpression(ast.props[propName]) || undefined;
|
||||
if (propName.includes(".")) {
|
||||
let [name, suffix] = propName.split(".");
|
||||
if (suffix === "bind") {
|
||||
this.helpers.add("bind");
|
||||
propName = name;
|
||||
propValue = `bind(ctx, ${propValue})`;
|
||||
} else {
|
||||
throw new Error("Invalid prop suffix");
|
||||
}
|
||||
}
|
||||
propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`;
|
||||
props.push(`${propName}: ${propValue}`);
|
||||
if (propName === "slots") {
|
||||
hasSlotsProp = true;
|
||||
}
|
||||
}
|
||||
|
||||
// slots
|
||||
@@ -1116,7 +1085,9 @@ export class CodeGenerator {
|
||||
params.push(`__scope: "${scope}"`);
|
||||
}
|
||||
if (ast.slots[slotName].attrs) {
|
||||
params.push(this.formatPropObject(ast.slots[slotName].attrs!));
|
||||
for (const [n, v] of Object.entries(ast.slots[slotName].attrs!)) {
|
||||
params.push(`${n}: ${compileExpr(v) || undefined}`);
|
||||
}
|
||||
}
|
||||
const slotInfo = `{${params.join(", ")}}`;
|
||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||
@@ -1125,17 +1096,18 @@ export class CodeGenerator {
|
||||
}
|
||||
|
||||
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
|
||||
this.helpers.add("markRaw");
|
||||
props.push(`slots: markRaw(${slotDef})`);
|
||||
props.push(`slots: ${slotDef}`);
|
||||
}
|
||||
|
||||
const propStr = `{${props.join(",")}}`;
|
||||
|
||||
let propString = propStr;
|
||||
if (ast.dynamicProps) {
|
||||
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
|
||||
props.length ? ", " + propStr : ""
|
||||
})`;
|
||||
if (!props.length) {
|
||||
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
|
||||
} else {
|
||||
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
|
||||
}
|
||||
}
|
||||
|
||||
let propVar: string;
|
||||
@@ -1146,8 +1118,7 @@ export class CodeGenerator {
|
||||
}
|
||||
|
||||
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
|
||||
this.helpers.add("markRaw");
|
||||
this.addLine(`${propVar!}.slots = markRaw(Object.assign(${slotDef}, ${propVar!}.slots))`);
|
||||
this.addLine(`${propVar!}.slots = Object.assign(${slotDef}, ${propVar!}.slots)`);
|
||||
}
|
||||
|
||||
// cmap key
|
||||
@@ -1195,7 +1166,15 @@ export class CodeGenerator {
|
||||
slotName = "'" + ast.name + "'";
|
||||
}
|
||||
|
||||
const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
|
||||
let scope = null;
|
||||
if (ast.attrs) {
|
||||
const params = [];
|
||||
for (const [n, v] of Object.entries(ast.attrs!)) {
|
||||
params.push(`${n}: ${compileExpr(v) || undefined}`);
|
||||
}
|
||||
scope = `{${params.join(", ")}}`;
|
||||
}
|
||||
|
||||
if (ast.defaultContent) {
|
||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
|
||||
|
||||
@@ -305,9 +305,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (tagName === "t" && !dynamicTag) {
|
||||
return null;
|
||||
}
|
||||
if (tagName.startsWith("block-")) {
|
||||
throw new Error(`Invalid tag name: '${tagName}'`);
|
||||
}
|
||||
ctx = Object.assign({}, ctx);
|
||||
if (tagName === "pre") {
|
||||
ctx.inPreTag = true;
|
||||
@@ -374,8 +371,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
ctx = Object.assign({}, ctx);
|
||||
ctx.tModelInfo = model;
|
||||
}
|
||||
} else if (attr.startsWith("block-")) {
|
||||
throw new Error(`Invalid attribute: '${attr}'`);
|
||||
} else if (attr !== "t-name") {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -682,9 +677,6 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
const dynamicProps = node.getAttribute("t-props");
|
||||
node.removeAttribute("t-props");
|
||||
|
||||
const defaultSlotScope = node.getAttribute("t-slot-scope");
|
||||
node.removeAttribute("t-slot-scope");
|
||||
|
||||
const props: ASTComponent["props"] = {};
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name)!;
|
||||
@@ -750,9 +742,6 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
const defaultContent = parseChildNodes(clone, ctx);
|
||||
if (defaultContent) {
|
||||
slots.default = { content: defaultContent };
|
||||
if (defaultSlotScope) {
|
||||
slots.default.scope = defaultSlotScope;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
|
||||
|
||||
@@ -36,7 +36,7 @@ export class Component<Props = any, Env = any> {
|
||||
|
||||
setup() {}
|
||||
|
||||
render(deep: boolean = false) {
|
||||
this.__owl__.render(deep);
|
||||
render() {
|
||||
this.__owl__.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { App, Env } from "../app/app";
|
||||
import { BDom, VNode } from "../blockdom";
|
||||
import { clearReactivesForCallback, Reactive, reactive, TARGET, NonReactive } from "../reactivity";
|
||||
import { batched, Callback } from "../utils";
|
||||
import { Component, ComponentConstructor } from "./component";
|
||||
import { fibersInError, handleError } from "./error_handling";
|
||||
import {
|
||||
Fiber,
|
||||
makeChildFiber,
|
||||
@@ -12,15 +9,13 @@ import {
|
||||
MountOptions,
|
||||
RootFiber,
|
||||
} from "./fibers";
|
||||
import { handleError, fibersInError } from "./error_handling";
|
||||
import { applyDefaultProps } from "./props_validation";
|
||||
import { STATUS } from "./status";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function getCurrent(): ComponentNode {
|
||||
if (!currentNode) {
|
||||
throw new Error("No active component (a hook function should only be called in 'setup')");
|
||||
}
|
||||
export function getCurrent(): ComponentNode | null {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
@@ -28,54 +23,13 @@ export function useComponent(): Component {
|
||||
return currentNode!.component;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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> | NonReactive<T> {
|
||||
const node = getCurrent();
|
||||
let render = batchedRenderFunctions.get(node)!;
|
||||
if (!render) {
|
||||
render = batched(node.render.bind(node));
|
||||
batchedRenderFunctions.set(node, render);
|
||||
// manual implementation of onWillDestroy to break cyclic dependency
|
||||
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||
}
|
||||
return reactive(state, render);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// component function (used in compiled template code)
|
||||
// -----------------------------------------------------------------------------
|
||||
type Props = { [key: string]: any };
|
||||
|
||||
function arePropsDifferent(props1: Props, props2: Props): boolean {
|
||||
for (let k in props1) {
|
||||
if (props1[k] !== props2[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Object.keys(props1).length !== Object.keys(props2).length;
|
||||
}
|
||||
|
||||
export function component<P extends object>(
|
||||
name: string | ComponentConstructor<P>,
|
||||
props: P,
|
||||
export function component(
|
||||
name: string | typeof Component,
|
||||
props: any,
|
||||
key: string,
|
||||
ctx: ComponentNode,
|
||||
parent: any
|
||||
): ComponentNode<P> {
|
||||
): ComponentNode {
|
||||
let node: any = ctx.children[key];
|
||||
let isDynamic = typeof name !== "string";
|
||||
|
||||
@@ -93,10 +47,7 @@ export function component<P extends object>(
|
||||
|
||||
const parentFiber = ctx.fiber!;
|
||||
if (node) {
|
||||
const currentProps = node.component.props[TARGET];
|
||||
if (parentFiber.deep || arePropsDifferent(currentProps, props)) {
|
||||
node.updateAndRender(props, parentFiber);
|
||||
}
|
||||
node.updateAndRender(props, parentFiber);
|
||||
} else {
|
||||
// new component
|
||||
let C;
|
||||
@@ -111,18 +62,19 @@ export function component<P extends object>(
|
||||
node = new ComponentNode(C, props, ctx.app, ctx);
|
||||
ctx.children[key] = node;
|
||||
|
||||
node.initiateRender(new Fiber(node, parentFiber));
|
||||
const fiber = makeChildFiber(node, parentFiber);
|
||||
node.initiateRender(fiber);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Component VNode class
|
||||
// Component VNode
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type LifecycleHook = Function;
|
||||
|
||||
export class ComponentNode<P extends object = any, E = any> implements VNode<ComponentNode<P, E>> {
|
||||
export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E>> {
|
||||
el?: HTMLElement | Text | undefined;
|
||||
app: App;
|
||||
fiber: Fiber | null = null;
|
||||
@@ -153,11 +105,9 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
||||
applyDefaultProps(props, C);
|
||||
const env = (parent && parent.childEnv) || app.env;
|
||||
this.childEnv = env;
|
||||
props = useState(props);
|
||||
this.component = new C(props, env, this) as any;
|
||||
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
||||
this.component.setup();
|
||||
currentNode = null;
|
||||
}
|
||||
|
||||
mountComponent(target: any, options?: MountOptions) {
|
||||
@@ -183,29 +133,21 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
||||
}
|
||||
}
|
||||
|
||||
async render(deep: boolean = false) {
|
||||
async render() {
|
||||
let current = this.fiber;
|
||||
if (current && current.root!.locked) {
|
||||
await Promise.resolve();
|
||||
// situation may have changed after the microtask tick
|
||||
current = this.fiber;
|
||||
}
|
||||
if (current) {
|
||||
if (!current.bdom && !fibersInError.has(current)) {
|
||||
if (deep) {
|
||||
// we want the render from this point on to be with deep=true
|
||||
current.deep = deep;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if current rendering was with deep=true, we want this one to be the same
|
||||
deep = deep || current.deep;
|
||||
} else if (!this.bdom) {
|
||||
if (current && !current.bdom && !fibersInError.has(current)) {
|
||||
return;
|
||||
}
|
||||
if (!this.bdom && !current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fiber = makeRootFiber(this);
|
||||
fiber.deep = deep;
|
||||
this.fiber = fiber;
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
await Promise.resolve();
|
||||
@@ -267,10 +209,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
||||
this.fiber = fiber;
|
||||
const component = this.component;
|
||||
applyDefaultProps(props, component.constructor as any);
|
||||
|
||||
currentNode = this;
|
||||
props = useState(props);
|
||||
currentNode = null;
|
||||
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
||||
await prom;
|
||||
if (fiber !== this.fiber) {
|
||||
@@ -335,14 +273,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
||||
}
|
||||
|
||||
patch() {
|
||||
if (this.fiber && this.fiber.parent) {
|
||||
// we only patch here renderings coming from above. renderings initiated
|
||||
// by the component will be patched independently in the appropriate
|
||||
// fiber.complete
|
||||
this._patch();
|
||||
}
|
||||
}
|
||||
_patch() {
|
||||
const hasChildren = Object.keys(this.children).length > 0;
|
||||
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
|
||||
if (hasChildren) {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { STATUS } from "./status";
|
||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||
let current = node.fiber;
|
||||
if (current) {
|
||||
cancelFibers(current.children);
|
||||
let root = parent.root;
|
||||
cancelFibers(root, current.children);
|
||||
current.root = null;
|
||||
}
|
||||
return new Fiber(node, parent);
|
||||
@@ -16,8 +17,9 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
||||
let current = node.fiber;
|
||||
if (current) {
|
||||
let root = current.root!;
|
||||
root.counter = root.counter + 1 - cancelFibers(current.children);
|
||||
root.counter -= cancelFibers(root, current.children);
|
||||
current.children = [];
|
||||
root.counter++;
|
||||
current.bdom = null;
|
||||
if (fibersInError.has(current)) {
|
||||
fibersInError.delete(current);
|
||||
@@ -33,20 +35,22 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
||||
if (node.patched.length) {
|
||||
fiber.patched.push(fiber);
|
||||
}
|
||||
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns number of not-yet rendered fibers cancelled
|
||||
*/
|
||||
function cancelFibers(fibers: Fiber[]): number {
|
||||
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(fiber.children);
|
||||
result += cancelFibers(root, fiber.children);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -58,13 +62,11 @@ export class Fiber {
|
||||
parent: Fiber | null;
|
||||
children: Fiber[] = [];
|
||||
appliedToDom = false;
|
||||
deep: boolean = false;
|
||||
|
||||
constructor(node: ComponentNode, parent: Fiber | null) {
|
||||
this.node = node;
|
||||
this.parent = parent;
|
||||
if (parent) {
|
||||
this.deep = parent.deep;
|
||||
const root = parent.root!;
|
||||
root.counter++;
|
||||
this.root = root;
|
||||
@@ -107,7 +109,7 @@ export class RootFiber extends Fiber {
|
||||
current = undefined;
|
||||
|
||||
// Step 2: patching the dom
|
||||
node._patch();
|
||||
node.patch();
|
||||
this.locked = false;
|
||||
|
||||
// Step 4: calling all mounted lifecycle hooks
|
||||
@@ -157,7 +159,6 @@ export class MountFiber extends RootFiber {
|
||||
let current: Fiber | undefined = this;
|
||||
try {
|
||||
const node = this.node;
|
||||
(node.app.constructor as any).validateTarget(this.target);
|
||||
if (node.bdom) {
|
||||
// this is a complicated situation: if we mount a fiber with an existing
|
||||
// bdom, this means that this same fiber was already completed, mounted,
|
||||
|
||||
@@ -1,102 +1,67 @@
|
||||
import { getCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers } from "./error_handling";
|
||||
|
||||
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
|
||||
cause: any;
|
||||
};
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result.catch((cause) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
|
||||
const node = getCurrent()!;
|
||||
node.willStart.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
|
||||
const node = getCurrent()!;
|
||||
node.willUpdateProps.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onMounted(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
|
||||
const node = getCurrent()!;
|
||||
node.mounted.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
|
||||
const node = getCurrent()!;
|
||||
node.willPatch.unshift(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onPatched(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
|
||||
const node = getCurrent()!;
|
||||
node.patched.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
|
||||
const node = getCurrent()!;
|
||||
node.willUnmount.unshift(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
|
||||
const node = getCurrent()!;
|
||||
node.willDestroy.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillRender(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
const renderFn = node.renderFn;
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.renderFn = decorate(() => {
|
||||
node.renderFn = () => {
|
||||
fn.call(node.component);
|
||||
return renderFn();
|
||||
}, "onWillRender");
|
||||
};
|
||||
}
|
||||
|
||||
export function onRendered(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
const renderFn = node.renderFn;
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.renderFn = decorate(() => {
|
||||
node.renderFn = () => {
|
||||
const result = renderFn();
|
||||
fn.call(node.component);
|
||||
return result;
|
||||
}, "onRendered");
|
||||
};
|
||||
}
|
||||
|
||||
type OnErrorCallback = (error: any) => void | any;
|
||||
export function onError(callback: OnErrorCallback) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
let handlers = nodeErrorHandlers.get(node);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
|
||||
@@ -47,7 +47,6 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
|
||||
}
|
||||
applyDefaultProps(props, ComponentClass);
|
||||
|
||||
const defaultProps = ComponentClass.defaultProps || {};
|
||||
let propsDef = getPropDescription(ComponentClass.props);
|
||||
const allowAdditionalProps = "*" in propsDef;
|
||||
|
||||
@@ -55,18 +54,8 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
|
||||
if (propName === "*") {
|
||||
continue;
|
||||
}
|
||||
const propDef = propsDef[propName];
|
||||
let isMandatory = !!propDef;
|
||||
if (typeof propDef === "object" && "optional" in propDef) {
|
||||
isMandatory = !propDef.optional;
|
||||
}
|
||||
if (isMandatory && propName in defaultProps) {
|
||||
throw new Error(
|
||||
`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`
|
||||
);
|
||||
}
|
||||
if ((props as any)[propName] === undefined) {
|
||||
if (isMandatory) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
||||
} else {
|
||||
continue;
|
||||
@@ -74,7 +63,7 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
|
||||
}
|
||||
let isValid;
|
||||
try {
|
||||
isValid = isValidProp((props as any)[propName], propDef);
|
||||
isValid = isValidProp((props as any)[propName], propsDef[propName]);
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
||||
(e as Error).message
|
||||
|
||||
+7
-6
@@ -11,7 +11,7 @@ import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks
|
||||
* html node or component.
|
||||
*/
|
||||
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
const refs = node.refs;
|
||||
return {
|
||||
get el(): T | null {
|
||||
@@ -29,7 +29,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
|
||||
* need a reference to the env of the component calling them.
|
||||
*/
|
||||
export function useEnv<E extends Env>(): E {
|
||||
return getCurrent().component.env as any;
|
||||
return getCurrent()!.component.env as any;
|
||||
}
|
||||
|
||||
function extendEnv(currentEnv: Object, extension: Object): Object {
|
||||
@@ -44,13 +44,14 @@ function extendEnv(currentEnv: Object, extension: Object): Object {
|
||||
* constructor method.
|
||||
*/
|
||||
export function useSubEnv(envExtension: Env) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
const newEnv = extendEnv(node.component.env as any, envExtension);
|
||||
node.component.env = extendEnv(node.component.env as any, envExtension);
|
||||
useChildSubEnv(envExtension);
|
||||
node.childEnv = newEnv;
|
||||
}
|
||||
|
||||
export function useChildSubEnv(envExtension: Env) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
node.childEnv = extendEnv(node.childEnv, envExtension);
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -121,7 +122,7 @@ export function useExternalListener(
|
||||
handler: EventListener,
|
||||
eventParams?: AddEventListenerOptions
|
||||
) {
|
||||
const node = getCurrent();
|
||||
const node = getCurrent()!;
|
||||
const boundHandler = handler.bind(node.component);
|
||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||
|
||||
+8
-3
@@ -1,3 +1,4 @@
|
||||
import { UTILS } from "./app/template_helpers";
|
||||
import {
|
||||
config,
|
||||
createBlock,
|
||||
@@ -12,10 +13,12 @@ import {
|
||||
comment,
|
||||
} from "./blockdom";
|
||||
import { mainEventHandler } from "./component/handler";
|
||||
import { Portal } from "./portal";
|
||||
export type { Reactive } from "./reactivity";
|
||||
|
||||
config.shouldNormalizeDom = false;
|
||||
config.mainEventHandler = mainEventHandler;
|
||||
(UTILS as any).Portal = Portal;
|
||||
|
||||
export const blockDom = {
|
||||
config,
|
||||
@@ -35,11 +38,13 @@ export const blockDom = {
|
||||
|
||||
export { App, mount } from "./app/app";
|
||||
export { Component } from "./component/component";
|
||||
export { useComponent, useState } from "./component/component_node";
|
||||
export { useComponent } from "./component/component_node";
|
||||
export { status } from "./component/status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { Memo } from "./memo";
|
||||
export { xml } from "./app/template_set";
|
||||
export { useState, reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||
export { EventBus, whenReady, loadFile, markup, xml } from "./utils";
|
||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { Component } from "./component/component";
|
||||
import type { ComponentNode } from "./component/component_node";
|
||||
import { xml } from "./app/template_set";
|
||||
import { Fiber } from "./component/fibers";
|
||||
|
||||
export class Memo extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
|
||||
constructor(props: any, env: any, node: ComponentNode) {
|
||||
super(props, env, node);
|
||||
|
||||
// prevent patching process conditionally
|
||||
let applyPatch = false;
|
||||
const patchFn = node.patch;
|
||||
node.patch = () => {
|
||||
if (applyPatch) {
|
||||
patchFn.call(node);
|
||||
applyPatch = false;
|
||||
}
|
||||
};
|
||||
|
||||
// check props change, and render/apply patch if it changed
|
||||
let prevProps = props;
|
||||
const updateAndRender = node.updateAndRender;
|
||||
node.updateAndRender = function (props: any, parentFiber: Fiber) {
|
||||
const shouldUpdate = !shallowEqual(prevProps, props);
|
||||
if (shouldUpdate) {
|
||||
prevProps = props;
|
||||
updateAndRender.call(node, props, parentFiber);
|
||||
applyPatch = true;
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* we assume that each object have the same set of keys
|
||||
*/
|
||||
function shallowEqual(p1: any, p2: any): boolean {
|
||||
for (let k in p1) {
|
||||
if (k !== "slots" && p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { onWillUnmount } from "./component/lifecycle_hooks";
|
||||
import { xml } from "./utils";
|
||||
import { xml } from "./app/template_set";
|
||||
import { BDom, text, VNode } from "./blockdom";
|
||||
import { Component } from "./component/component";
|
||||
|
||||
|
||||
+81
-245
@@ -1,42 +1,27 @@
|
||||
import { Callback } from "./utils";
|
||||
import { onWillDestroy } from "./component/lifecycle_hooks";
|
||||
import { ComponentNode, getCurrent } from "./component/component_node";
|
||||
import { batched, Callback } from "./utils";
|
||||
|
||||
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
|
||||
export const TARGET = Symbol("Target");
|
||||
const TARGET = Symbol("Target");
|
||||
// Escape hatch to prevent reactivity system to turn something into a reactive
|
||||
const SKIP = Symbol("Skip");
|
||||
// Special key to subscribe to, to be notified of key creation/deletion
|
||||
const KEYCHANGES = Symbol("Key changes");
|
||||
|
||||
type ObjectKey = string | number | symbol;
|
||||
|
||||
type Target = object;
|
||||
type Collection = Set<any> | Map<any, any> | WeakMap<any, any>;
|
||||
type CollectionRawType = "Set" | "Map" | "WeakMap";
|
||||
|
||||
export type Reactive<T extends Target = Target> = T & {
|
||||
[TARGET]: any;
|
||||
};
|
||||
|
||||
export type NonReactive<T extends Target = Target> = T & {
|
||||
type NonReactive<T extends Target = Target> = T & {
|
||||
[SKIP]: any;
|
||||
};
|
||||
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
|
||||
/**
|
||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||
* many native objects such as Promise (whose toString is [object Promise])
|
||||
* or Date ([object Date]), while also supporting collections without using
|
||||
* instanceof in a loop
|
||||
*
|
||||
* @param obj the object to check
|
||||
* @returns the raw type of the object
|
||||
*/
|
||||
function rawType(obj: any) {
|
||||
return objectToString.call(obj).slice(8, -1);
|
||||
}
|
||||
/**
|
||||
* Checks whether a given value can be made into a reactive object.
|
||||
*
|
||||
@@ -47,17 +32,11 @@ function canBeMadeReactive(value: any): boolean {
|
||||
if (typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||
}
|
||||
/**
|
||||
* Creates a reactive from the given object/callback if possible and returns it,
|
||||
* returns the original object otherwise.
|
||||
*
|
||||
* @param value the value make reactive
|
||||
* @returns a reactive for the given object when possible, the original otherwise
|
||||
*/
|
||||
function possiblyReactive(val: any, cb: Callback) {
|
||||
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||||
// extract "RawType" from strings like "[object RawType]" => this lets us
|
||||
// ignore many native objects such as Promise (whose toString is [object Promise])
|
||||
// or Date ([object Date]).
|
||||
const rawType = objectToString.call(value).slice(8, -1);
|
||||
return rawType === "Object" || rawType === "Array";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,10 +57,10 @@ export function markRaw<T extends Target>(value: T): NonReactive<T> {
|
||||
* @returns the underlying value
|
||||
*/
|
||||
export function toRaw<T extends object>(value: Reactive<T>): T {
|
||||
return value[TARGET] || value;
|
||||
return value[TARGET];
|
||||
}
|
||||
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
|
||||
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.
|
||||
@@ -91,7 +70,7 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
|
||||
* or deletion)
|
||||
* @param callback the function to call when the key changes
|
||||
*/
|
||||
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
|
||||
if (!targetToKeysToCallbacks.get(target)) {
|
||||
targetToKeysToCallbacks.set(target, new Map());
|
||||
}
|
||||
@@ -114,7 +93,7 @@ function observeTargetKey(target: Target, key: PropertyKey, callback: Callback):
|
||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||
* or deleted)
|
||||
*/
|
||||
function notifyReactives(target: Target, key: PropertyKey): void {
|
||||
function notifyReactives(target: Target, key: ObjectKey): void {
|
||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||
if (!keyToCallbacks) {
|
||||
return;
|
||||
@@ -136,7 +115,7 @@ const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
||||
*
|
||||
* @param callback the callback for which the reactives need to be cleared
|
||||
*/
|
||||
export function clearReactivesForCallback(callback: Callback): void {
|
||||
function clearReactivesForCallback(callback: Callback): void {
|
||||
const targetsToClear = callbacksToTargets.get(callback);
|
||||
if (!targetsToClear) {
|
||||
return;
|
||||
@@ -200,218 +179,75 @@ export function reactive<T extends Target>(
|
||||
}
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||
: basicProxyHandler<T>(callback);
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
const proxy = new Proxy(target, {
|
||||
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
const value = Reflect.get(target, key, proxy);
|
||||
if (!canBeMadeReactive(value)) {
|
||||
return value;
|
||||
}
|
||||
return reactive(value, callback);
|
||||
},
|
||||
set(target, key, value, proxy) {
|
||||
const isNewKey = !Object.hasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, proxy);
|
||||
const ret = Reflect.set(target, key, value, proxy);
|
||||
if (isNewKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
// While Array length may trigger the set trap, it's not actually set by this
|
||||
// method but is updated behind the scenes, and the trap is not called with the
|
||||
// new value. We disable the "same-value-optimization" for it because of that.
|
||||
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const ret = Reflect.deleteProperty(target, key);
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
notifyReactives(target, key);
|
||||
return ret;
|
||||
},
|
||||
ownKeys(target) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
has(target, key) {
|
||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
});
|
||||
reactivesForTarget.set(callback, proxy);
|
||||
}
|
||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||
}
|
||||
|
||||
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||
/**
|
||||
* Creates a basic proxy handler for regular objects and arrays.
|
||||
* 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 callback @see reactive
|
||||
* @returns a proxy handler object
|
||||
* @param state the state to observe
|
||||
* @returns a reactive object that will cause the component to re-render on
|
||||
* relevant changes
|
||||
* @see reactive
|
||||
*/
|
||||
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
||||
return {
|
||||
get(target: any, key: PropertyKey, proxy: Reactive<T>) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(Reflect.get(target, key, proxy), callback);
|
||||
},
|
||||
set(target, key, value, proxy) {
|
||||
const isNewKey = !objectHasOwnProperty.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);
|
||||
// TODO: only notify when something was actually deleted
|
||||
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
|
||||
// observing the key itself would observe value changes instead of presence changes
|
||||
// so we may need a finer grained system to distinguish observing value vs presence.
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
} as ProxyHandler<T>;
|
||||
}
|
||||
/**
|
||||
* Creates a function that will observe the key that is passed to it when called
|
||||
* and delegates to the underlying method.
|
||||
*
|
||||
* @param methodName name of the method to delegate to
|
||||
* @param target @see reactive
|
||||
* @param callback @see reactive
|
||||
*/
|
||||
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
|
||||
return (key: any) => {
|
||||
key = toRaw(key);
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[methodName](key), callback);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates an iterable that will delegate to the underlying iteration method and
|
||||
* observe keys as necessary.
|
||||
*
|
||||
* @param methodName name of the method to delegate to
|
||||
* @param target @see reactive
|
||||
* @param callback @see reactive
|
||||
*/
|
||||
function makeIteratorObserver(
|
||||
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
||||
target: any,
|
||||
callback: Callback
|
||||
) {
|
||||
return function* () {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
const keys = target.keys();
|
||||
for (const item of target[methodName]()) {
|
||||
const key = keys.next().value;
|
||||
observeTargetKey(target, key, callback);
|
||||
yield possiblyReactive(item, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates a function that will delegate to an underlying method, and check if
|
||||
* that method has modified the presence or value of a key, and notify the
|
||||
* reactives appropriately.
|
||||
*
|
||||
* @param setterName name of the method to delegate to
|
||||
* @param getterName name of the method which should be used to retrieve the
|
||||
* value before calling the delegate method for comparison purposes
|
||||
* @param target @see reactive
|
||||
*/
|
||||
function delegateAndNotify(
|
||||
setterName: "set" | "add" | "delete",
|
||||
getterName: "has" | "get",
|
||||
target: any
|
||||
) {
|
||||
return (key: any, value: any) => {
|
||||
key = toRaw(key);
|
||||
const hadKey = target.has(key);
|
||||
const originalValue = target[getterName](key);
|
||||
const ret = target[setterName](key, value);
|
||||
const hasKey = target.has(key);
|
||||
if (hadKey !== hasKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
if (originalValue !== value) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates a function that will clear the underlying collection and notify that
|
||||
* the keys of the collection have changed.
|
||||
*
|
||||
* @param target @see reactive
|
||||
*/
|
||||
function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
||||
return () => {
|
||||
const allKeys = [...target.keys()];
|
||||
target.clear();
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
for (const key of allKeys) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Maps raw type of an object to an object containing functions that can be used
|
||||
* to build an appropritate proxy handler for that raw type. Eg: when making a
|
||||
* reactive set, calling the has method should mark the key that is being
|
||||
* retrieved as observed, and calling the add or delete method should notify the
|
||||
* reactives that the key which is being added or deleted has been modified.
|
||||
*/
|
||||
const rawTypeToFuncHandlers = {
|
||||
Set: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
add: delegateAndNotify("add", "has", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
Map: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
WeakMap: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
}),
|
||||
};
|
||||
/**
|
||||
* Creates a proxy handler for collections (Set/Map/WeakMap)
|
||||
*
|
||||
* @param callback @see reactive
|
||||
* @param target @see reactive
|
||||
* @returns a proxy handler object
|
||||
*/
|
||||
function collectionsProxyHandler<T extends Collection>(
|
||||
target: T,
|
||||
callback: Callback,
|
||||
targetRawType: CollectionRawType
|
||||
): ProxyHandler<T> {
|
||||
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||||
// property is read.
|
||||
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
||||
return Object.assign(basicProxyHandler(callback), {
|
||||
get(target: any, key: PropertyKey) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||||
return (specialHandlers as any)[key];
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[key], callback);
|
||||
},
|
||||
}) as ProxyHandler<T>;
|
||||
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||
const node = getCurrent()!;
|
||||
if (!batchedRenderFunctions.has(node)) {
|
||||
batchedRenderFunctions.set(
|
||||
node,
|
||||
batched(() => node.render())
|
||||
);
|
||||
onWillDestroy(() => clearReactivesForCallback(render));
|
||||
}
|
||||
const render = batchedRenderFunctions.get(node)!;
|
||||
const reactiveState = reactive(state, render);
|
||||
return reactiveState;
|
||||
}
|
||||
|
||||
@@ -24,15 +24,6 @@ export function batched(callback: Callback): Callback {
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTarget(target: HTMLElement) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||
}
|
||||
if (!document.body.contains(target)) {
|
||||
throw new Error("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
}
|
||||
|
||||
export class EventBus extends EventTarget {
|
||||
trigger(name: string, payload?: any) {
|
||||
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
||||
@@ -71,17 +62,3 @@ export class Markup extends String {}
|
||||
export function markup(value: any) {
|
||||
return new Markup(value);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// xml tag helper
|
||||
// -----------------------------------------------------------------------------
|
||||
export const globalTemplates: { [key: string]: string | Element } = {};
|
||||
|
||||
export function xml(...args: Parameters<typeof String.raw>) {
|
||||
const name = `__template__${xml.nextId++}`;
|
||||
const value = String.raw(...args);
|
||||
globalTemplates[name] = value;
|
||||
return name;
|
||||
}
|
||||
|
||||
xml.nextId = 1;
|
||||
|
||||
@@ -169,15 +169,6 @@ describe("properties", () => {
|
||||
expect(input.value).toBe("potato");
|
||||
});
|
||||
|
||||
test("input with value attribute, and undefined given", () => {
|
||||
const block = createBlock(`<input block-attribute-0="value"/>`);
|
||||
|
||||
const tree = block([undefined]);
|
||||
mount(tree, fixture);
|
||||
const input = fixture.querySelector("input")!;
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
|
||||
test("input type=checkbox with checked attribute", () => {
|
||||
// render input with initial value
|
||||
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createBlock, mount, patch, remove } from "../../src/blockdom";
|
||||
import { logStep } from "../helpers";
|
||||
import { makeTestFixture } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -56,15 +55,3 @@ test("is in dom when callback is called", async () => {
|
||||
|
||||
mount(tree, fixture);
|
||||
});
|
||||
|
||||
test("callback ref in callback ref with same block", async () => {
|
||||
const block = createBlock('<p block-ref="0"><block-text-1/><block-child-0/></p>');
|
||||
let refFn = (el: HTMLParagraphElement) => logStep(el.outerHTML);
|
||||
|
||||
const child = block([refFn, "child"], []);
|
||||
const parent = block([refFn, "parent"], [child]);
|
||||
mount(parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<p>parent<p>child</p></p>");
|
||||
expect(["<p>child</p>", "<p>parent<p>child</p></p>"]).toBeLogged();
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ exports[`misc complex template 1`] = `
|
||||
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);
|
||||
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);
|
||||
@@ -316,8 +316,8 @@ exports[`misc other complex template 1`] = `
|
||||
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 + \`__2\`, node, ctx);
|
||||
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev, search: ctx['search']}, key + \`__3\`, node, ctx);
|
||||
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__2\`, node, ctx);
|
||||
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__3\`, node, ctx);
|
||||
b25 = block25([], [b26, b27]);
|
||||
}
|
||||
return block1([attr1, txt1, hdlr2, hdlr3, attr8, hdlr4, hdlr5, ref1, hdlr6, ref2], [b2, b4, b14, b17, b22, b23, b24, b25]);
|
||||
|
||||
@@ -407,36 +407,6 @@ exports[`t-out variable 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-out with a String class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = safeOutput(ctx['var']);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-out with an extended String class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = safeOutput(ctx['var']);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-raw is deprecated should warn 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`translation support can set and remove translatable attributes 1`] = `
|
||||
exports[`translation support can set translatable attributes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"mot\\" label=\\"word\\">text</div>\`);
|
||||
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"word\\">text</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { renderToString } from "../helpers";
|
||||
|
||||
describe("blacklisted tags and attributes", () => {
|
||||
test("template with block-text tag", () => {
|
||||
const template = `<div><block-text-0/>hello</div>`;
|
||||
expect(() => renderToString(template)).toThrow("Invalid tag name: 'block-text-0'");
|
||||
});
|
||||
|
||||
test("template with block-handler tag", () => {
|
||||
const template = `<div block-handler-0="click">hello</div>`;
|
||||
expect(() => renderToString(template)).toThrow("Invalid attribute: 'block-handler-0'");
|
||||
});
|
||||
});
|
||||
@@ -37,26 +37,6 @@ describe("t-out", () => {
|
||||
expect(renderToString(template, { var: "ok" })).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("with a String class", () => {
|
||||
const template = `<span><t t-out="var"/></span>`;
|
||||
expect(renderToString(template, { var: new String("ok") })).toBe("<span>ok</span>");
|
||||
});
|
||||
|
||||
test("with an extended String class", () => {
|
||||
class LoveString extends String {
|
||||
valueOf(): string {
|
||||
return `<3 ${super.valueOf()} <3`;
|
||||
}
|
||||
toString(): string {
|
||||
return this.valueOf();
|
||||
}
|
||||
}
|
||||
const template = `<span><t t-out="var"/></span>`;
|
||||
expect(renderToString(template, { var: new LoveString("ok") })).toBe(
|
||||
"<span><3 ok <3</span>"
|
||||
);
|
||||
});
|
||||
|
||||
test("not escaping", () => {
|
||||
const template = `<div><t t-out="var"/></div>`;
|
||||
expect(renderToString(template, { var: markup("<ok></ok>") })).toBe("<div><ok></ok></div>");
|
||||
|
||||
@@ -59,20 +59,18 @@ describe("translation support", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("can set and remove translatable attributes", async () => {
|
||||
test("can set translatable attributes", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div tomato="word" potato="word" title="word" label="word">text</div>
|
||||
<div tomato="word" potato="word" title="word">text</div>
|
||||
`;
|
||||
}
|
||||
|
||||
await mount(SomeComponent, fixture, {
|
||||
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
|
||||
translatableAttributes: ["potato", "-label"],
|
||||
translatableAttributes: ["potato"],
|
||||
});
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div tomato="word" potato="mot" title="mot" label="word">text</div>'
|
||||
);
|
||||
expect(fixture.innerHTML).toBe('<div tomato="word" potato="mot" title="word">text</div>');
|
||||
});
|
||||
|
||||
test("translation is done on the trimmed text, with extra spaces readded after", async () => {
|
||||
|
||||
@@ -81,19 +81,6 @@ exports[`basics a class component inside a class component, no external dom 2`]
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics a component cannot be mounted in a detached node (even if node is detached later) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics a component inside a component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
@@ -994,7 +981,7 @@ exports[`basics update props of component without concrete own node 2`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const tKey_1 = ctx['props'].subKey;
|
||||
return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key, subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx));
|
||||
return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key,subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -183,7 +183,7 @@ exports[`concurrent renderings scenario 1 2`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -227,7 +227,7 @@ exports[`concurrent renderings scenario 2 2`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -270,7 +270,7 @@ exports[`concurrent renderings scenario 2bis 2`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -327,7 +327,7 @@ exports[`concurrent renderings scenario 3 3`] = `
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -384,7 +384,7 @@ exports[`concurrent renderings scenario 4 3`] = `
|
||||
let block1 = createBlock(\`<span><block-child-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -557,7 +557,7 @@ exports[`concurrent renderings scenario 9 3`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -734,7 +734,7 @@ exports[`concurrent renderings scenario 14 2`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -778,7 +778,7 @@ exports[`concurrent renderings scenario 15 2`] = `
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
@@ -817,7 +817,7 @@ exports[`concurrent renderings scenario 16 2`] = `
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
return component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1067,33 +1067,6 @@ exports[`delay willUpdateProps with rendering grandchild 4`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroying/recreating a subcomponent, other scenario 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2,b3;
|
||||
b2 = text(\`parent\`);
|
||||
if (ctx['state'].hasChild) {
|
||||
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroying/recreating a subcomponent, other scenario 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`child\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroying/recreating a subwidget with different props (if start is not over) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
@@ -1172,7 +1145,7 @@ exports[`properly behave when destroyed/unmounted while rendering 2`] = `
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`SubChild\`, {}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -85,22 +85,10 @@ exports[`basics simple catchError 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors calling a hook outside setup should crash 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['state'].value);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors can catch an error in a component render function 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -109,7 +97,7 @@ exports[`can catch errors can catch an error in a component render function 1`]
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -153,7 +141,6 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -162,7 +149,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -192,7 +179,6 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -203,7 +189,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b5 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
|
||||
let b5 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
|
||||
return block1([], [b5]);
|
||||
}
|
||||
}"
|
||||
@@ -272,7 +258,6 @@ exports[`can catch errors can catch an error in the initial call of a component
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -281,7 +266,7 @@ exports[`can catch errors can catch an error in the initial call of a component
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -325,7 +310,6 @@ exports[`can catch errors can catch an error in the initial call of a component
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -336,7 +320,7 @@ exports[`can catch errors can catch an error in the initial call of a component
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3;
|
||||
if (ctx['state'].flag) {
|
||||
b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
}
|
||||
return block1([], [b3]);
|
||||
}
|
||||
@@ -470,7 +454,6 @@ exports[`can catch errors can catch an error in the mounted call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -479,7 +462,7 @@ exports[`can catch errors can catch an error in the mounted call 1`] = `
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -522,7 +505,6 @@ exports[`can catch errors can catch an error in the willPatch call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><span><block-text-0/></span><block-child-0/></div>\`);
|
||||
|
||||
@@ -532,7 +514,7 @@ exports[`can catch errors can catch an error in the willPatch call 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['state'].message;
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([txt1], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -576,7 +558,6 @@ exports[`can catch errors can catch an error in the willStart call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -585,7 +566,7 @@ exports[`can catch errors can catch an error in the willStart call 1`] = `
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
@@ -628,7 +609,6 @@ exports[`can catch errors can catch an error origination from a child's willStar
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -639,7 +619,7 @@ exports[`can catch errors can catch an error origination from a child's willStar
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b5 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
|
||||
let b5 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
|
||||
return block1([], [b5]);
|
||||
}
|
||||
}"
|
||||
@@ -742,7 +722,7 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, capture, markRaw, withKey } = helpers;
|
||||
let { prepareList, capture, withKey } = helpers;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
let Comp1 = ctx['cp'].Comp;
|
||||
@@ -757,7 +737,7 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
|
||||
let key1 = ctx['cp'].id;
|
||||
const v1 = ctx['cp'];
|
||||
const ctx1 = capture(ctx);
|
||||
c_block1[i1] = withKey(component(\`ErrorHandler\`, {onError: ()=>this.cleanUp(v1.id),slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, ctx), key1);
|
||||
c_block1[i1] = withKey(component(\`ErrorHandler\`, {onError: ()=>this.cleanUp(v1.id),slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
@@ -817,7 +797,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, capture, markRaw, withKey } = helpers;
|
||||
let { prepareList, capture, withKey } = helpers;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
let Comp1 = ctx['elem'][1];
|
||||
@@ -832,7 +812,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
|
||||
let key1 = ctx['elem'][0];
|
||||
const v1 = ctx['elem'];
|
||||
const ctx1 = capture(ctx);
|
||||
c_block1[i1] = withKey(component(\`Catch\`, {onError: (_error)=>this.onError(v1[0],_error),slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, ctx), key1);
|
||||
c_block1[i1] = withKey(component(\`Catch\`, {onError: (_error)=>this.onError(v1[0],_error),slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
@@ -882,7 +862,6 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
@@ -892,7 +871,7 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`OK\`, {}, key + \`__1\`, node, ctx);
|
||||
let b4 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
|
||||
let b4 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
|
||||
return block1([], [b2, b4]);
|
||||
}
|
||||
}"
|
||||
@@ -1102,19 +1081,6 @@ exports[`errors and promises an error in mounted call will reject the mount prom
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`errors and promises an error in onMounted callback will have the component's setup in its stack trace 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>abc</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`errors and promises an error in patched call will reject the render promise 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -103,37 +103,6 @@ exports[`hooks mounted callbacks should be called in reverse order from willUnmo
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hooks parent and child env (with useChildSubEnv then useSubEnv) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['env'].val);
|
||||
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hooks parent and child env (with useChildSubEnv then useSubEnv) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block2 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (ctx['env'].hasParent) {
|
||||
let txt1 = ctx['env'].val;
|
||||
b2 = block2([txt1]);
|
||||
}
|
||||
return multi([b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hooks parent and child env (with useChildSubEnv) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -527,7 +527,7 @@ exports[`lifecycle hooks onWillRender 1`] = `
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
|
||||
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -273,7 +273,7 @@ exports[`bound functions is referentially equal after update 1`] = `
|
||||
let { bind } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {val: ctx['state'].val, fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx);
|
||||
return component(\`Child\`, {val: ctx['state'].val,fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`default props a default prop cannot be defined on a mandatory prop 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {};
|
||||
helpers.validateProps(\`Child\`, props1, ctx);
|
||||
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`default props can set default boolean values 1`] = `
|
||||
exports[`default props can set default required boolean values 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
@@ -29,7 +16,7 @@ exports[`default props can set default boolean values 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`default props can set default boolean values 2`] = `
|
||||
exports[`default props can set default required boolean values 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
@@ -115,7 +102,7 @@ exports[`props validation can specify that additional props are allowed (array)
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {message: 'm', otherProp: 'o'};
|
||||
const props1 = {message: 'm',otherProp: 'o'};
|
||||
helpers.validateProps(\`Child\`, props1, ctx);
|
||||
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
|
||||
}
|
||||
@@ -141,7 +128,7 @@ exports[`props validation can specify that additional props are allowed (object)
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {message: 'm', otherProp: 'o'};
|
||||
const props1 = {message: 'm',otherProp: 'o'};
|
||||
helpers.validateProps(\`Child\`, props1, ctx);
|
||||
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
|
||||
}
|
||||
|
||||
@@ -28,20 +28,6 @@ exports[`reactivity in lifecycle can use a state hook 2 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`reactivity in lifecycle can use a state hook on Map 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['counter'].get('value');
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`reactivity in lifecycle change state while mounting component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -38,31 +38,11 @@ exports[`refs can use 2 refs with same name in a t-if/t-else situation 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`refs refs and recursive templates 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p block-ref=\\"0\\"><block-text-1/><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const refs = ctx.__owl__.refs;
|
||||
const ref1 = (el) => refs[\`root\`] = el;
|
||||
let b2;
|
||||
let txt1 = ctx['props'].tree.value;
|
||||
if (ctx['props'].tree.child) {
|
||||
b2 = component(\`Test\`, {tree: ctx['props'].tree.child}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
return block1([ref1, txt1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`refs refs are properly bound in slots 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
let { capture } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><span class=\\"counter\\"><block-text-0/></span><block-child-0/></div>\`);
|
||||
let block2 = createBlock(\`<button block-handler-0=\\"click\\" block-ref=\\"1\\">do something</button>\`);
|
||||
@@ -77,7 +57,7 @@ exports[`refs refs are properly bound in slots 1`] = `
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['state'].val;
|
||||
const ctx1 = capture(ctx);
|
||||
let b3 = component(\`Dialog\`, {slots: markRaw({'footer': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx);
|
||||
let b3 = component(\`Dialog\`, {slots: {'footer': {__render: slot1, __ctx: ctx1}}}, key + \`__1\`, node, ctx);
|
||||
return block1([txt1], [b3]);
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`force render in case of existing render 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`B\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`force render in case of existing render 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`C\`, {}, key + \`__1\`, node, ctx);
|
||||
let b3 = text(ctx['props'].val);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`force render in case of existing render 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`C\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics can force a render to update sub tree 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['state'].value);
|
||||
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics can force a render to update sub tree 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`child\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics can render a parent without rendering child 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['state'].value);
|
||||
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics can render a parent without rendering child 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`child\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics props are reactive (nested prop) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics props are reactive (nested prop) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].a.b.c);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics props are reactive 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics props are reactive 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].a.b);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics render with deep=true followed by render with deep=false work as expected 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(\`parent\`);
|
||||
let b3 = text(ctx['state'].value);
|
||||
let b4 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3, b4]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics render with deep=true followed by render with deep=false work as expected 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(\`child\`);
|
||||
let b3 = text(ctx['env'].getValue());
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics rendering is atomic (for one subtree) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['state'].obj.val);
|
||||
let b3 = component(\`B\`, {obj: ctx['state'].obj}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics rendering is atomic (for one subtree) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`C\`, {obj: ctx['props'].obj}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics rendering is atomic (for one subtree) 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].obj.val);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics works as expected for dynamic number of props 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering semantics works as expected for dynamic number of props 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(Object.keys(ctx['props']).length);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -147,7 +147,7 @@ exports[`style and class handling class on sub component, which is switched to a
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {class: 'someclass', child: ctx['state'].child}, key + \`__1\`, node, ctx);
|
||||
return component(\`Child\`, {class: 'someclass',child: ctx['state'].child}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -177,7 +177,7 @@ exports[`list of components reconciliation alg works for t-foreach in t-foreach,
|
||||
for (let i2 = 0; i2 < l_block4; i2++) {
|
||||
ctx[\`col\`] = v_block4[i2];
|
||||
let key2 = ctx['col'];
|
||||
let b6 = component(\`Child\`, {row: ctx['row'], col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx);
|
||||
let b6 = component(\`Child\`, {row: ctx['row'],col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx);
|
||||
c_block4[i2] = withKey(block5([], [b6]), key2);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
|
||||
@@ -410,24 +410,6 @@ exports[`t-model directive on an textarea 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model on an input with an undefined value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { toNumber } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<input block-attribute-0=\\"value\\" block-handler-1=\\"input\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const bExpr1 = ctx['state'];
|
||||
const expr1 = 'text';
|
||||
let attr1 = bExpr1[expr1];
|
||||
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
|
||||
return block1([attr1, hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model on select with static options 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -117,7 +117,7 @@ exports[`t-props t-props with props 1`] = `
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1, b: 2}), key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1,b: 2}), key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue, capture, markRaw } = helpers;
|
||||
let { isBoundary, withDefault, setContextValue, capture } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><p><block-text-0/></p><block-child-0/><p><block-text-1/></p></div>\`);
|
||||
|
||||
@@ -21,7 +21,7 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 1`] = `
|
||||
setContextValue(ctx, \\"iter\\", 'source');
|
||||
let txt1 = ctx['iter'];
|
||||
const ctx1 = capture(ctx);
|
||||
let b2 = component(\`Childcomp\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx);
|
||||
let b2 = component(\`Childcomp\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1\`, node, ctx);
|
||||
let txt2 = ctx['iter'];
|
||||
return block1([txt1, txt2], [b2]);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ exports[`t-set slots with a t-set with a component in body 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, isBoundary, withDefault, LazyValue, safeOutput, markRaw } = helpers;
|
||||
let { capture, isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
@@ -68,7 +68,7 @@ exports[`t-set slots with a t-set with a component in body 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2\`, node, ctx);
|
||||
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -102,7 +102,7 @@ exports[`t-set slots with an t-set with a component in body 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, isBoundary, withDefault, LazyValue, safeOutput, markRaw } = helpers;
|
||||
let { capture, isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
||||
|
||||
let block4 = createBlock(\`<div>coffee</div>\`);
|
||||
|
||||
@@ -123,7 +123,7 @@ exports[`t-set slots with an t-set with a component in body 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return component(\`Blorg\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2\`, node, ctx);
|
||||
return component(\`Blorg\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -157,7 +157,7 @@ exports[`t-set slots with an unused t-set with a component in body 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { capture, isBoundary, withDefault, LazyValue, markRaw } = helpers;
|
||||
let { capture, isBoundary, withDefault, LazyValue } = helpers;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
@@ -172,7 +172,7 @@ exports[`t-set slots with an unused t-set with a component in body 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2\`, node, ctx);
|
||||
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Component, mount, status, toRaw, useState, xml } from "../../src";
|
||||
import { App, Component, mount, status, useState, xml } from "../../src";
|
||||
import { elem, makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
||||
import { markup } from "../../src/utils";
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("basics", () => {
|
||||
class Test extends Component {
|
||||
static template = xml`<span>simple vnode</span>`;
|
||||
setup() {
|
||||
expect(toRaw(this.props)).toBe(p);
|
||||
expect(this.props).toBe(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,27 +171,6 @@ describe("basics", () => {
|
||||
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
|
||||
});
|
||||
|
||||
test("a component cannot be mounted in a detached node (even if node is detached later)", async () => {
|
||||
const warn = console.warn;
|
||||
console.warn = jest.fn();
|
||||
class Test extends Component {
|
||||
static template = xml`<div/>`;
|
||||
}
|
||||
let error: Error;
|
||||
const prom = mount(Test, fixture);
|
||||
await Promise.resolve();
|
||||
fixture.remove();
|
||||
try {
|
||||
await prom;
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
|
||||
expect(console.warn).toBeCalledTimes(1);
|
||||
console.warn = warn;
|
||||
});
|
||||
|
||||
test("crashes if it cannot find a template", async () => {
|
||||
class Test extends Component {
|
||||
static template = "wrongtemplate";
|
||||
@@ -204,7 +183,7 @@ describe("basics", () => {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe('Missing template: "wrongtemplate" (for component "Test")');
|
||||
expect(error!.message).toBe('Missing template: "wrongtemplate"');
|
||||
});
|
||||
|
||||
test("class component with dynamic text", async () => {
|
||||
|
||||
@@ -39,8 +39,6 @@ Scheduler.prototype.addFiber = function (fiber: Fiber) {
|
||||
|
||||
afterEach(() => {
|
||||
if (lastScheduler && lastScheduler.tasks.size > 0) {
|
||||
// we still clear the scheduler to prevent additional noise
|
||||
lastScheduler.tasks.clear();
|
||||
throw new Error("we got a memory leak...");
|
||||
}
|
||||
});
|
||||
@@ -133,58 +131,6 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("destroying/recreating a subcomponent, other scenario", async () => {
|
||||
let flag = false;
|
||||
|
||||
class Child extends Component {
|
||||
static template = xml`child`;
|
||||
setup() {
|
||||
if (!flag) {
|
||||
flag = true;
|
||||
parent.render(true);
|
||||
}
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`parent<Child t-if="state.hasChild"/>`;
|
||||
static components = { Child };
|
||||
state = useState({ hasChild: false });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("parent");
|
||||
|
||||
parent.state.hasChild = true;
|
||||
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:mounted",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
|
||||
expect(fixture.innerHTML).toBe("parentchild");
|
||||
});
|
||||
|
||||
test("creating two async components, scenario 1", async () => {
|
||||
let defA = makeDeferred();
|
||||
let defB = makeDeferred();
|
||||
@@ -575,7 +521,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
|
||||
}
|
||||
|
||||
class Child extends Component {
|
||||
static template = xml`<div><SubChild val="props.val"/></div>`;
|
||||
static template = xml`<div><SubChild /></div>`;
|
||||
static components = { SubChild };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
@@ -1961,13 +1907,18 @@ test("concurrent renderings scenario 13", async () => {
|
||||
await nextTick(); // wait for this change to be applied
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willPatch",
|
||||
"Child:mounted",
|
||||
"Child:patched",
|
||||
"Parent:patched",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
@@ -2521,9 +2472,9 @@ test("two renderings initiated between willPatch and patched", async () => {
|
||||
useLogLifecycle();
|
||||
onMounted(() => {
|
||||
this.mounted = "Mounted";
|
||||
parent.render(true);
|
||||
parent.render();
|
||||
});
|
||||
onWillUnmount(() => parent.render(true));
|
||||
onWillUnmount(() => parent.render());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2556,11 +2507,15 @@ test("two renderings initiated between willPatch and patched", async () => {
|
||||
"Parent:rendered",
|
||||
]).toBeLogged();
|
||||
|
||||
await nextMicroTick();
|
||||
expect(["Panel:willRender", "Panel:rendered"]).toBeLogged();
|
||||
|
||||
await nextTick();
|
||||
expect(["Parent:willPatch", "Panel:willPatch", "Panel:patched", "Parent:patched"]).toBeLogged();
|
||||
expect([
|
||||
"Panel:willRender",
|
||||
"Panel:rendered",
|
||||
"Parent:willPatch",
|
||||
"Panel:willPatch",
|
||||
"Panel:patched",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("<div><abc>Panel1Mounted</abc></div>");
|
||||
|
||||
parent.state.panel = "Panel2";
|
||||
@@ -2798,20 +2753,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
||||
static template = xml`<Parent state="state"/>`;
|
||||
static components = { Parent };
|
||||
state = { value: 0 };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(GrandParent, fixture);
|
||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||
expect([
|
||||
"GrandParent:setup",
|
||||
"GrandParent:willStart",
|
||||
"GrandParent:willRender",
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"GrandParent:rendered",
|
||||
"Parent:willRender",
|
||||
"DelayedChild:setup",
|
||||
"DelayedChild:willStart",
|
||||
@@ -2825,23 +2772,20 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
||||
"ReactiveChild:mounted",
|
||||
"DelayedChild:mounted",
|
||||
"Parent:mounted",
|
||||
"GrandParent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
promise = makeDeferred();
|
||||
const prom1 = promise;
|
||||
parent.state.value = 1;
|
||||
child.render(); // trigger a root rendering first
|
||||
parent.render(true);
|
||||
parent.render();
|
||||
reactiveChild.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||
expect([
|
||||
"DelayedChild:willRender",
|
||||
"DelayedChild:rendered",
|
||||
"GrandParent:willRender",
|
||||
"Parent:willUpdateProps",
|
||||
"GrandParent:rendered",
|
||||
"ReactiveChild:willRender",
|
||||
"ReactiveChild:rendered",
|
||||
"Parent:willRender",
|
||||
@@ -2856,14 +2800,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
||||
const prom2 = promise;
|
||||
child.render(); // trigger a root rendering first
|
||||
parent.state.value = 2;
|
||||
parent.render(true);
|
||||
parent.render();
|
||||
reactiveChild.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||
expect([
|
||||
"GrandParent:willRender",
|
||||
"Parent:willUpdateProps",
|
||||
"GrandParent:rendered",
|
||||
"ReactiveChild:willRender",
|
||||
"ReactiveChild:rendered",
|
||||
"Parent:willRender",
|
||||
@@ -2880,14 +2822,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
||||
expect([
|
||||
"DelayedChild:willRender",
|
||||
"DelayedChild:rendered",
|
||||
"GrandParent:willPatch",
|
||||
"Parent:willPatch",
|
||||
"ReactiveChild:willPatch",
|
||||
"DelayedChild:willPatch",
|
||||
"DelayedChild:patched",
|
||||
"ReactiveChild:patched",
|
||||
"Parent:patched",
|
||||
"GrandParent:patched",
|
||||
]).toBeLogged();
|
||||
|
||||
prom1.resolve();
|
||||
|
||||
@@ -82,6 +82,8 @@ describe("basics", () => {
|
||||
});
|
||||
|
||||
test("display a nice error if it cannot find component (in dev mode)", async () => {
|
||||
const info = console.info;
|
||||
console.info = jest.fn(() => {}); // dev mode message
|
||||
class SomeComponent extends Component {}
|
||||
class Parent extends Component {
|
||||
static template = xml`<SomeMispelledComponent />`;
|
||||
@@ -89,7 +91,7 @@ describe("basics", () => {
|
||||
}
|
||||
let error: Error;
|
||||
try {
|
||||
await mount(Parent, fixture, { test: true });
|
||||
await mount(Parent, fixture, { dev: true });
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
@@ -98,6 +100,8 @@ describe("basics", () => {
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
expect(console.info).toBeCalledTimes(1);
|
||||
console.info = info;
|
||||
});
|
||||
|
||||
test("simple catchError", async () => {
|
||||
@@ -175,30 +179,6 @@ describe("errors and promises", () => {
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("an error in onMounted callback will have the component's setup in its stack trace", async () => {
|
||||
class App extends Component {
|
||||
static template = xml`<div>abc</div>`;
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let error: Error;
|
||||
try {
|
||||
await mount(App, fixture, { test: true });
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.stack).toContain("App.setup");
|
||||
expect(error!.stack).toContain("error_handling.test.ts");
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("an error in willPatch call will reject the render promise", async () => {
|
||||
class Root extends Component {
|
||||
static template = xml`<div><t t-esc="val"/></div>`;
|
||||
@@ -211,13 +191,13 @@ describe("errors and promises", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture, { test: true });
|
||||
const root = await mount(Root, fixture);
|
||||
root.val = 4;
|
||||
let error: Error;
|
||||
root.render();
|
||||
await nextTick();
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(`The following error occurred in onWillPatch: "boom"`);
|
||||
expect(error!.message).toBe("boom");
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -234,13 +214,13 @@ describe("errors and promises", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture, { test: true });
|
||||
const root = await mount(Root, fixture);
|
||||
root.val = 4;
|
||||
let error: Error;
|
||||
root.render();
|
||||
await nextTick();
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(`The following error occurred in onPatched: "boom"`);
|
||||
expect(error!.message).toBe("boom");
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -316,6 +296,7 @@ describe("errors and promises", () => {
|
||||
});
|
||||
|
||||
test("errors in mounted and in willUnmount", async () => {
|
||||
expect.assertions(4);
|
||||
class Example extends Component {
|
||||
static template = xml`<div/>`;
|
||||
val: any;
|
||||
@@ -332,11 +313,9 @@ describe("errors and promises", () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await mount(Example, fixture, { test: true });
|
||||
await mount(Example, fixture);
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe(
|
||||
`The following error occurred in onMounted: "Error in mounted"`
|
||||
);
|
||||
expect((e as Error).message).toBe("Error in mounted");
|
||||
}
|
||||
// 1 additional error is logged because the destruction of the app causes
|
||||
// the onWillUnmount hook to be called and to fail
|
||||
@@ -395,28 +374,6 @@ describe("can catch errors", () => {
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("calling a hook outside setup should crash", async () => {
|
||||
class Root extends Component {
|
||||
static template = xml`<t t-esc="state.value"/>`;
|
||||
state = useState({ value: 1 });
|
||||
|
||||
setup() {
|
||||
onWillStart(() => {
|
||||
this.state = useState({ value: 2 });
|
||||
});
|
||||
}
|
||||
}
|
||||
let e: Error;
|
||||
try {
|
||||
await mount(Root, fixture, { test: true });
|
||||
} catch (error) {
|
||||
e = error as Error;
|
||||
}
|
||||
expect(e!.message).toBe(
|
||||
`The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
|
||||
);
|
||||
});
|
||||
|
||||
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
|
||||
|
||||
@@ -238,7 +238,7 @@ describe("hooks", () => {
|
||||
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
||||
someVal = "brain";
|
||||
someVal2 = "maggot";
|
||||
component.render(true);
|
||||
component.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
||||
});
|
||||
@@ -272,7 +272,7 @@ describe("hooks", () => {
|
||||
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
||||
someVal = "brain";
|
||||
someVal2 = "maggot";
|
||||
component.render(true);
|
||||
component.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
||||
});
|
||||
@@ -322,24 +322,6 @@ describe("hooks", () => {
|
||||
expect(fixture.innerHTML).toBe("3<div>5</div>");
|
||||
});
|
||||
|
||||
test("parent and child env (with useChildSubEnv then useSubEnv)", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<div t-if="env.hasParent"><t t-esc="env.val"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-esc="env.val"/><Child/>`;
|
||||
static components = { Child };
|
||||
setup() {
|
||||
useChildSubEnv({ hasParent: true });
|
||||
useSubEnv({ val: 5 });
|
||||
}
|
||||
}
|
||||
const env = { val: 3 };
|
||||
await mount(Parent, fixture, { env });
|
||||
expect(fixture.innerHTML).toBe("5<div>5</div>");
|
||||
});
|
||||
|
||||
test("can use onWillStart, onWillUpdateProps", async () => {
|
||||
const steps: string[] = [];
|
||||
async function slow(): Promise<string> {
|
||||
|
||||
@@ -849,9 +849,8 @@ describe("lifecycle hooks", () => {
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child someValue="state.value" />`;
|
||||
<Child />`;
|
||||
static components = { Child };
|
||||
state = useState({ value: 1 });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
@@ -872,7 +871,7 @@ describe("lifecycle hooks", () => {
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.value++; // to block child render
|
||||
parent.render(); // to block child render
|
||||
await nextTick();
|
||||
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
|
||||
|
||||
@@ -1009,15 +1008,20 @@ describe("lifecycle hooks", () => {
|
||||
await nextTick();
|
||||
expect([
|
||||
"C:willRender",
|
||||
"D:willUpdateProps",
|
||||
"F:setup",
|
||||
"F:willStart",
|
||||
"C:rendered",
|
||||
"D:willRender",
|
||||
"D:rendered",
|
||||
"F:willRender",
|
||||
"F:rendered",
|
||||
"C:willPatch",
|
||||
"D:willPatch",
|
||||
"E:willUnmount",
|
||||
"E:willDestroy",
|
||||
"F:mounted",
|
||||
"D:patched",
|
||||
"C:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ let mockConsoleWarn: any;
|
||||
|
||||
beforeAll(() => {
|
||||
console.info = (message: any) => {
|
||||
if (message === DEV_MSG()) {
|
||||
if (message === DEV_MSG) {
|
||||
return;
|
||||
}
|
||||
info(message);
|
||||
@@ -684,7 +684,7 @@ describe("props validation", () => {
|
||||
test("default values are applied before validating props at update", async () => {
|
||||
// need to do something about errors catched in render
|
||||
class SubComp extends Component {
|
||||
static props = { p: { type: Number, optional: true } };
|
||||
static props = { p: { type: Number } };
|
||||
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
@@ -791,9 +791,9 @@ describe("default props", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||
});
|
||||
|
||||
test("can set default boolean values", async () => {
|
||||
test("can set default required boolean values", async () => {
|
||||
class SubComp extends Component {
|
||||
static props = ["p?", "q?"];
|
||||
static props = ["p", "q"];
|
||||
static defaultProps = { p: true, q: false };
|
||||
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
|
||||
}
|
||||
@@ -804,28 +804,4 @@ describe("default props", () => {
|
||||
await mount(Parent, fixture, { dev: true });
|
||||
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
|
||||
});
|
||||
|
||||
test("a default prop cannot be defined on a mandatory prop", async () => {
|
||||
class Child extends Component {
|
||||
static props = {
|
||||
mandatory: Number,
|
||||
};
|
||||
static defaultProps = { mandatory: 3 };
|
||||
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`<Child/>`;
|
||||
}
|
||||
let error: Error;
|
||||
try {
|
||||
await mount(Parent, fixture, { dev: true });
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,18 +53,6 @@ describe("reactivity in lifecycle", () => {
|
||||
expect(n).toBe(2); // no new rendering occured: b was never read via state!
|
||||
});
|
||||
|
||||
test("can use a state hook on Map", async () => {
|
||||
class Counter extends Component {
|
||||
static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
|
||||
counter = useState(new Map([["value", 42]]));
|
||||
}
|
||||
const counter = await mount(Counter, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||
counter.counter.set("value", 3);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
test("state changes in willUnmount do not trigger rerender", async () => {
|
||||
const steps: string[] = [];
|
||||
class Child extends Component {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, mount, onMounted, useRef, useState } from "../../src/index";
|
||||
import { logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
import { Component, mount, useRef, useState } from "../../src/index";
|
||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
import { xml } from "../../src/index";
|
||||
|
||||
snapshotEverything();
|
||||
@@ -100,27 +100,4 @@ describe("refs", () => {
|
||||
expect(console.warn).toBeCalledTimes(1);
|
||||
console.warn = consoleWarn;
|
||||
});
|
||||
|
||||
test("refs and recursive templates", async () => {
|
||||
class Test extends Component {
|
||||
static components = {};
|
||||
static template = xml`
|
||||
<p t-ref="root">
|
||||
<t t-esc="props.tree.value"/>
|
||||
<t t-if="props.tree.child"><Test tree="props.tree.child"/></t>
|
||||
</p>`;
|
||||
root = useRef("root");
|
||||
|
||||
setup() {
|
||||
onMounted(() => logStep(this.root.el!.outerHTML));
|
||||
}
|
||||
}
|
||||
Test.components = { Test };
|
||||
|
||||
const tree = { value: "a", child: { value: "b", child: null } };
|
||||
|
||||
await mount(Test, fixture, { props: { tree } });
|
||||
expect(fixture.innerHTML).toBe("<p>a<p>b</p></p>");
|
||||
expect(["<p>b</p>", "<p>a<p>b</p></p>"]).toBeLogged();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src";
|
||||
import {
|
||||
makeTestFixture,
|
||||
snapshotEverything,
|
||||
nextTick,
|
||||
useLogLifecycle,
|
||||
makeDeferred,
|
||||
nextMicroTick,
|
||||
} from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("rendering semantics", () => {
|
||||
test("can render a parent without rendering child", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`child`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.value"/>
|
||||
<Child/>
|
||||
`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ value: "A" });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("Achild");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.value = "B";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("can force a render to update sub tree", async () => {
|
||||
let childN = 0;
|
||||
let parentN = 0;
|
||||
class Child extends Component {
|
||||
static template = xml`child`;
|
||||
setup() {
|
||||
onRendered(() => childN++);
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.value"/>
|
||||
<Child/>
|
||||
`;
|
||||
static components = { Child };
|
||||
|
||||
state = { value: "A" };
|
||||
setup() {
|
||||
onRendered(() => parentN++);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("Achild");
|
||||
expect(parentN).toBe(1);
|
||||
expect(childN).toBe(1);
|
||||
|
||||
parent.state.value = "B";
|
||||
parent.render(true);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
expect(parentN).toBe(2);
|
||||
expect(childN).toBe(2);
|
||||
});
|
||||
|
||||
test("render with deep=true followed by render with deep=false work as expected", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`child<t t-esc="env.getValue()"/>`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`parent<t t-esc="state.value"/><Child/>`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ value: "A" });
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
let value = 3;
|
||||
const env = {
|
||||
getValue() {
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
const parent = await mount(Parent, fixture, { env });
|
||||
|
||||
expect(fixture.innerHTML).toBe("parentAchild3");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
value = 4;
|
||||
parent.render(true);
|
||||
|
||||
// wait for child to be rendered, but dom not yet patched
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.value = "B";
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("parentBchild4");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("props are reactive", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.a.b"/>`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child a="state"/>
|
||||
`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ b: 1 });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.b = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("3");
|
||||
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
});
|
||||
|
||||
test("props are reactive (nested prop)", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.a.b.c"/>`;
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child a="state"/>
|
||||
`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ b: { c: 1 } });
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.b.c = 3; // parent is now subscribed to 'b' key
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("3");
|
||||
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
|
||||
parent.state.b = { c: 444 }; // triggers a parent and a child render
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("444");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("works as expected for dynamic number of props", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="Object.keys(props).length"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child t-props="state"/>
|
||||
`;
|
||||
static components = { Child };
|
||||
|
||||
state: any = useState({ b: 1 });
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
parent.state.newkey = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
});
|
||||
|
||||
test("rendering is atomic (for one subtree)", async () => {
|
||||
const def = makeDeferred();
|
||||
|
||||
class C extends Component {
|
||||
static template = xml`<t t-esc="props.obj.val"/>`;
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends Component {
|
||||
static template = xml`<C obj="props.obj"/>`;
|
||||
static components = { C };
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onWillUpdateProps(() => def);
|
||||
}
|
||||
}
|
||||
|
||||
class A extends Component {
|
||||
static template = xml`<t t-esc="state.obj.val"/><B obj="state.obj"/>`;
|
||||
static components = { B };
|
||||
|
||||
state = useState({ obj: { val: 1 } });
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("11");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
parent.state.obj.val = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("33");
|
||||
expect([
|
||||
"A:willRender",
|
||||
"A:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"A:patched",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
]).toBeLogged();
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
expect([]).toBeLogged();
|
||||
});
|
||||
});
|
||||
|
||||
test("force render in case of existing render", async () => {
|
||||
const def = makeDeferred();
|
||||
|
||||
class C extends Component {
|
||||
static template = xml`C`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
class B extends Component {
|
||||
static template = xml`<C/><t t-esc="props.val"/>`;
|
||||
static components = { C };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onWillUpdateProps(() => def);
|
||||
}
|
||||
}
|
||||
class A extends Component {
|
||||
static template = xml`<B val="state.val"/>`;
|
||||
static components = { B };
|
||||
state = useState({ val: 1 });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
const parent = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("C1");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
// trigger a new rendering, blocked in B
|
||||
parent.state.val = 2;
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
||||
|
||||
// initiate a new render with deep=true. it should cancel the current render
|
||||
// and also be blocked in B
|
||||
parent.render(true);
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
// we check here that the render reaches C (so, that it was properly forced)
|
||||
expect([
|
||||
"B:willRender",
|
||||
"C:willUpdateProps",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"B:willPatch",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
"B:patched",
|
||||
"A:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
@@ -45,36 +45,7 @@ describe("slots", () => {
|
||||
expect(fixture.innerHTML).toBe("some text");
|
||||
});
|
||||
|
||||
test("simple slot with slot scope", async () => {
|
||||
let child: any;
|
||||
class Child extends Component {
|
||||
static template = xml`<span><t t-slot="slotName" bool="state.bool"/></span>`;
|
||||
state = useState({ bool: true });
|
||||
setup() {
|
||||
child = this;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child>
|
||||
<t t-set-slot="slotName" t-slot-scope="slotScope">
|
||||
<t t-if="slotScope.bool">some text</t>
|
||||
<t t-else="slotScope.bool">other text</t>
|
||||
</t>
|
||||
</Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<span>some text</span>");
|
||||
|
||||
child.state.bool = false;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<span>other text</span>");
|
||||
});
|
||||
|
||||
test("default slot with slot scope: shorthand syntax", async () => {
|
||||
test("simple default slot with params", async () => {
|
||||
let child: any;
|
||||
class Child extends Component {
|
||||
static template = xml`<span><t t-slot="default" bool="state.bool"/></span>`;
|
||||
@@ -86,9 +57,11 @@ describe("slots", () => {
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child t-slot-scope="slotScope">
|
||||
<t t-if="slotScope.bool">some text</t>
|
||||
<t t-else="slotScope.bool">other text</t>
|
||||
<Child>
|
||||
<t t-set-slot="default" t-slot-scope="slotScope">
|
||||
<t t-if="slotScope.bool">some text</t>
|
||||
<t t-else="slotScope.bool">other text</t>
|
||||
</t>
|
||||
</Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
@@ -126,41 +99,6 @@ describe("slots", () => {
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("simple default slot with params and bound function", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-slot="default" fn.bind="getValue"/>`;
|
||||
state = useState({ value: 123 });
|
||||
getValue() {
|
||||
return this.state.value;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child t-slot-scope="slotScope"><t t-esc="slotScope.fn()"/></Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("123");
|
||||
});
|
||||
|
||||
test("default slot with params with - in it", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-slot="default" some-value="state.value"/>`;
|
||||
state = useState({ value: 123 });
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child t-slot-scope="slotScope"><t t-esc="slotScope['some-value']"/></Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("123");
|
||||
});
|
||||
|
||||
test("fun: two calls to the same slot", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-slot="default"/><t t-slot="default"/>`;
|
||||
@@ -277,30 +215,6 @@ describe("slots", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("can define and call slots with bound params", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`
|
||||
<t t-slot="abc"/>
|
||||
<t t-esc="props.slots['abc'].getValue()"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`
|
||||
<Child>
|
||||
<t t-set-slot="abc" getValue.bind="getValue">abc</t>
|
||||
</Child>`;
|
||||
state = useState({ value: 444 });
|
||||
getValue() {
|
||||
return this.state.value;
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("abc444");
|
||||
});
|
||||
|
||||
test("no named slot content => just no children", async () => {
|
||||
class Dialog extends Component {
|
||||
static template = xml`<span><t t-slot="header"/></span>`;
|
||||
@@ -1675,60 +1589,4 @@ describe("slots", () => {
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>SlotDisplay</div><div>Parent</div>");
|
||||
});
|
||||
|
||||
test("mix of slots, t-call, t-call with body, and giving own props child", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
class C extends Component {
|
||||
static template = xml`[C]<t t-slot="default" />`;
|
||||
}
|
||||
class B extends Component {
|
||||
static template = xml`[B]<C slots="props.slots" />`;
|
||||
static components = { C };
|
||||
}
|
||||
|
||||
const subTemplate2 = xml`[sub2<t t-esc="v"/>]`;
|
||||
const subTemplate1 = xml`[sub1]
|
||||
<t t-set="dummy" t-value="validate"/>
|
||||
<t t-call="${subTemplate2}">
|
||||
<t t-set="v" t-value="props.number"/>
|
||||
</t>`;
|
||||
|
||||
let a: any;
|
||||
class A extends Component {
|
||||
static components = { B };
|
||||
static template = xml`<B>[A]<t t-call="${subTemplate1}"/></B>`;
|
||||
setup() {
|
||||
a = this;
|
||||
}
|
||||
|
||||
get validate() {
|
||||
// we check here that the actual component was not lost somehow
|
||||
expect(this.__owl__.component === a).toBe(true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class P extends Component {
|
||||
static components = { A };
|
||||
static template = xml`<button t-on-click="inc">inc</button><A number="state.number"/>`;
|
||||
|
||||
state = useState({ number: 333 });
|
||||
inc() {
|
||||
this.state.number++;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<P/>`;
|
||||
static components = { P };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<button>inc</button>[B][C][A][sub1] [sub2333]");
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<button>inc</button>[B][C][A][sub1] [sub2334]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,19 +30,6 @@ describe("t-model directive", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||
});
|
||||
|
||||
test("t-model on an input with an undefined value", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<input t-model="state.text"/>`;
|
||||
state = useState({ text: undefined });
|
||||
}
|
||||
await mount(SomeComponent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<input>");
|
||||
|
||||
const input = fixture.querySelector("input")!;
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
|
||||
test("basic use, on an input with bracket expression", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("t-props", () => {
|
||||
});
|
||||
|
||||
test("basic use", async () => {
|
||||
expect.assertions(5);
|
||||
expect.assertions(4);
|
||||
|
||||
let props = { a: 1, b: 2 };
|
||||
|
||||
@@ -65,7 +65,6 @@ describe("t-props", () => {
|
||||
`;
|
||||
setup() {
|
||||
expect(this.props).toEqual({ a: 1, b: 2 });
|
||||
expect(this.props).not.toBe(props);
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
|
||||
+3
-4
@@ -15,11 +15,10 @@ import {
|
||||
useComponent,
|
||||
xml,
|
||||
} from "../src";
|
||||
import { helpers } from "../src/app/template_helpers";
|
||||
import { TemplateSet } from "../src/app/template_set";
|
||||
import { UTILS } from "../src/app/template_helpers";
|
||||
import { globalTemplates, TemplateSet } from "../src/app/template_set";
|
||||
import { BDom } from "../src/blockdom";
|
||||
import { compile } from "../src/compiler";
|
||||
import { globalTemplates } from "../src/utils";
|
||||
|
||||
const mount = blockDom.mount;
|
||||
|
||||
@@ -92,7 +91,7 @@ export function renderToBdom(template: string, context: any = {}, node?: any): B
|
||||
snapshottedTemplates.add(template);
|
||||
expect(fn.toString()).toMatchSnapshot();
|
||||
}
|
||||
return fn(blockDom, helpers)(context, node);
|
||||
return fn(blockDom, UTILS)(context, node);
|
||||
}
|
||||
|
||||
export function renderToString(template: string, context: any = {}, node?: any): string {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Memo if no prop change, prevent renderings from above 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
let b6 = text(ctx['state'].a);
|
||||
let b7 = text(ctx['state'].b);
|
||||
let b8 = text(ctx['state'].c);
|
||||
return multi([b6, b7, b8]);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['state'].a);
|
||||
let b3 = text(ctx['state'].b);
|
||||
let b4 = text(ctx['state'].c);
|
||||
let b9 = component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b3, b4, b9]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Memo if no props, prevent renderings from above 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
return component(\`Child\`, {value: ctx['state'].value}, key + \`__2\`, node, ctx);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
|
||||
let b4 = component(\`Memo\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
|
||||
return multi([b2, b4]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Memo if no props, prevent renderings from above 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].value);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Memo if no props, prevent renderings from above (work with simple html) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
return text(ctx['state'].value);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['state'].value);
|
||||
let b4 = component(\`Memo\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
|
||||
return multi([b2, b4]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -149,7 +149,6 @@ exports[`Portal Portal composed with t-slot 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { markRaw } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
@@ -158,7 +157,7 @@ exports[`Portal Portal composed with t-slot 1`] = `
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
|
||||
let b3 = component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Component, mount, useState, xml } from "../../src";
|
||||
import { Memo } from "../../src/";
|
||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("Memo", () => {
|
||||
test("if no props, prevent renderings from above ", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.value"/>`;
|
||||
}
|
||||
class Test extends Component {
|
||||
static template = xml`
|
||||
<Child value="state.value"/>
|
||||
<Memo>
|
||||
<Child value="state.value"/>
|
||||
</Memo>`;
|
||||
|
||||
static components = { Memo, Child };
|
||||
|
||||
state = useState({ value: 1 });
|
||||
}
|
||||
|
||||
const component = await mount(Test, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("11");
|
||||
component.state.value = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("21");
|
||||
});
|
||||
|
||||
test("if no props, prevent renderings from above (work with simple html) ", async () => {
|
||||
class Test extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.value"/>
|
||||
<Memo>
|
||||
<t t-esc="state.value"/>
|
||||
</Memo>`;
|
||||
|
||||
static components = { Memo };
|
||||
|
||||
state = useState({ value: 1 });
|
||||
}
|
||||
|
||||
const component = await mount(Test, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("11");
|
||||
component.state.value = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("21");
|
||||
});
|
||||
|
||||
test("if no prop change, prevent renderings from above ", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.value"/>`;
|
||||
}
|
||||
|
||||
class Test extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.a"/>
|
||||
<t t-esc="state.b"/>
|
||||
<t t-esc="state.c"/>
|
||||
<Memo a="state.a" b="state.b">
|
||||
<t t-esc="state.a"/>
|
||||
<t t-esc="state.b"/>
|
||||
<t t-esc="state.c"/>
|
||||
</Memo>`;
|
||||
|
||||
static components = { Memo, Child };
|
||||
|
||||
state = useState({ a: "a", b: "b", c: "c" });
|
||||
}
|
||||
|
||||
const component = await mount(Test, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("abcabc");
|
||||
|
||||
component.state.c = "C";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("abCabc");
|
||||
|
||||
component.state.a = "A";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("AbCAbC");
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ snapshotEverything();
|
||||
|
||||
beforeAll(() => {
|
||||
console.info = (message: any) => {
|
||||
if (message === DEV_MSG()) {
|
||||
if (message === DEV_MSG) {
|
||||
return;
|
||||
}
|
||||
info(message);
|
||||
@@ -421,7 +421,6 @@ describe("Portal", () => {
|
||||
addOutsideDiv(fixture);
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(steps).toEqual(["parent:mounted"]);
|
||||
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
||||
|
||||
parent.state.hasChild = true;
|
||||
await nextTick();
|
||||
@@ -431,7 +430,6 @@ describe("Portal", () => {
|
||||
"child:mounted",
|
||||
"parent:patched",
|
||||
]);
|
||||
expect(fixture.innerHTML).toBe('<div id="outside"><span>1</span></div><div></div>');
|
||||
|
||||
parent.state.val = 2;
|
||||
await nextTick();
|
||||
@@ -445,7 +443,6 @@ describe("Portal", () => {
|
||||
"child:patched",
|
||||
"parent:patched",
|
||||
]);
|
||||
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
||||
|
||||
parent.state.hasChild = false;
|
||||
await nextTick();
|
||||
@@ -462,7 +459,6 @@ describe("Portal", () => {
|
||||
"child:willUnmount",
|
||||
"parent:patched",
|
||||
]);
|
||||
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
||||
});
|
||||
|
||||
test("portal destroys on crash", async () => {
|
||||
|
||||
+11
-482
@@ -171,8 +171,6 @@ describe("Reactivity", () => {
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
// Skipped because the hasOwnProperty trap is tripped by *writing*. We
|
||||
// (probably) do not want to subscribe to changes on writes.
|
||||
test.skip("hasOwnProperty causes the key's presence to be observed", async () => {
|
||||
let n = 0;
|
||||
const state = createReactive({}, () => n++);
|
||||
@@ -1095,482 +1093,18 @@ describe("Reactivity", () => {
|
||||
expect(n).toBe(1);
|
||||
expect(state.k).toEqual({ n: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Collections", () => {
|
||||
describe("Set", () => {
|
||||
test("can make reactive Set", () => {
|
||||
const set = new Set<number>();
|
||||
const obj = reactive(set);
|
||||
expect(obj).not.toBe(set);
|
||||
});
|
||||
test("can add collections set/weakset/map/weakmap in a reactive object", () => {
|
||||
const rawSet = new Set();
|
||||
const rawWeakSet = new WeakSet();
|
||||
const rawMap = new Map();
|
||||
const rawWeakMap = new WeakMap();
|
||||
|
||||
test("can read", async () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.has(0)).toBe(false);
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const state = reactive(new Set());
|
||||
state.add(1);
|
||||
expect(state.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
state.delete(1);
|
||||
expect(state.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
test("can clear entries", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect(state.size).toBe(1);
|
||||
state.clear();
|
||||
expect(state.size).toBe(0);
|
||||
});
|
||||
|
||||
test("act like a Set", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect([...state.entries()]).toEqual([[1, 1]]);
|
||||
expect([...state.values()]).toEqual([1]);
|
||||
expect([...state.keys()]).toEqual([1]);
|
||||
expect([...state]).toEqual([1]); // Checks Symbol.iterator
|
||||
expect(state.size).toBe(1);
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(Set);
|
||||
});
|
||||
|
||||
test("reactive Set contains its keys", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
expect(state.has(state.keys().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Set contains its values", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
expect(state.has(state.values().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Set contains its entries' keys and values", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
const [key, val] = state.entries().next().value;
|
||||
expect(state.has(key)).toBe(true);
|
||||
expect(state.has(val)).toBe(true);
|
||||
});
|
||||
|
||||
test("checking for a key subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([1]), observer);
|
||||
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.add(2);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.add(2);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.add(3); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("iterating on keys returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state.keys().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on values returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state.values().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on entries returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const [reactiveObj, reactiveObj2] = state.entries().next().value;
|
||||
expect(reactiveObj2).toBe(reactiveObj);
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on reactive Set returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state[Symbol.iterator]().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WeakSet", () => {
|
||||
test("cannot make reactive WeakSet", () => {
|
||||
const set = new WeakSet();
|
||||
expect(() => reactive(set)).toThrowError("Cannot make the given value reactive");
|
||||
});
|
||||
|
||||
test("WeakSet in reactive is original WeakSet", () => {
|
||||
const obj = { set: new WeakSet() };
|
||||
const state = reactive(obj);
|
||||
expect(state.set).toBe(obj.set);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Map", () => {
|
||||
test("can make reactive Map", () => {
|
||||
const map = new Map();
|
||||
const obj = reactive(map);
|
||||
expect(obj).not.toBe(map);
|
||||
});
|
||||
|
||||
test("can read", async () => {
|
||||
const state = reactive(new Map([[1, 0]]));
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.has(0)).toBe(false);
|
||||
expect(state.get(1)).toBe(0);
|
||||
expect(state.get(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const state = reactive(new Map());
|
||||
state.set(1, 2);
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.get(1)).toBe(2);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
state.delete(1);
|
||||
expect(state.has(1)).toBe(false);
|
||||
expect(state.get(1)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can clear entries", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
expect(state.size).toBe(1);
|
||||
state.clear();
|
||||
expect(state.size).toBe(0);
|
||||
});
|
||||
|
||||
test("act like a Map", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
expect([...state.entries()]).toEqual([[1, 2]]);
|
||||
expect([...state.values()]).toEqual([2]);
|
||||
expect([...state.keys()]).toEqual([1]);
|
||||
expect([...state]).toEqual([[1, 2]]); // Checks Symbol.iterator
|
||||
expect(state.size).toBe(1);
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
test("reactive Map contains its keys", () => {
|
||||
const state = reactive(new Map([[{}, 1]]));
|
||||
expect(state.has(state.keys().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Map values are equal to doing a get on the appropriate key", () => {
|
||||
const state = reactive(new Map([[1, {}]]));
|
||||
expect(state.get(1)).toBe(state.values().next().value);
|
||||
});
|
||||
|
||||
test("reactive Map contains its entries' keys, and the associated value is the same as doing get", () => {
|
||||
const state = reactive(new Map([[{}, {}]]));
|
||||
const [key, val] = state.entries().next().value;
|
||||
expect(state.has(key)).toBe(true);
|
||||
expect(val).toBe(state.get(key));
|
||||
});
|
||||
|
||||
test("checking for a key with 'has' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, 2]]), observer);
|
||||
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(2, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(2, 3);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.set(3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("checking for a key with 'get' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, 2]]), observer);
|
||||
|
||||
expect(state.get(2)).toBeUndefined(); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(2, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.get(2)).toBe(3); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.delete(2); // deleting again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(2, 3);
|
||||
expect(state.get(2)).toBe(3); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.get(2)).toBeUndefined(); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.set(3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("getting values returns a reactive", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, obj]]), observer);
|
||||
const reactiveObj = state.get(1)!;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on values returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, obj]]), observer);
|
||||
const reactiveObj = state.values().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on keys returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[obj, 1]]), observer);
|
||||
const reactiveObj = state.keys().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on reactive map returns reactives", async () => {
|
||||
const keyObj = { a: 2 };
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[keyObj, valObj]]), observer);
|
||||
const [reactiveKeyObj, reactiveValObj] = state[Symbol.iterator]().next().value;
|
||||
expect(reactiveKeyObj).not.toBe(keyObj);
|
||||
expect(reactiveValObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
|
||||
expect(toRaw(reactiveValObj as any)).toBe(valObj);
|
||||
reactiveKeyObj.a = 0;
|
||||
reactiveValObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveKeyObj.a; // observe key "a" in key sub-reactive;
|
||||
reactiveKeyObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveValObj.a; // observe key "a" in val sub-reactive;
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
reactiveKeyObj.a = 1; // setting same value again shouldn't notify
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("iterating on entries returns reactives", async () => {
|
||||
const keyObj = { a: 2 };
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[keyObj, valObj]]), observer);
|
||||
const [reactiveKeyObj, reactiveValObj] = state.entries().next().value;
|
||||
expect(reactiveKeyObj).not.toBe(keyObj);
|
||||
expect(reactiveValObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
|
||||
expect(toRaw(reactiveValObj as any)).toBe(valObj);
|
||||
reactiveKeyObj.a = 0;
|
||||
reactiveValObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveKeyObj.a; // observe key "a" in key sub-reactive;
|
||||
reactiveKeyObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveValObj.a; // observe key "a" in val sub-reactive;
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
reactiveKeyObj.a = 1; // setting same value again shouldn't notify
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WeakMap", () => {
|
||||
test("can make reactive WeakMap", () => {
|
||||
const map = new WeakMap();
|
||||
const obj = reactive(map);
|
||||
expect(obj).not.toBe(map);
|
||||
});
|
||||
|
||||
test("can read", async () => {
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const state = reactive(new WeakMap([[obj, 0]]));
|
||||
expect(state.has(obj)).toBe(true);
|
||||
expect(state.has(obj2)).toBe(false);
|
||||
expect(state.get(obj)).toBe(0);
|
||||
expect(state.get(obj2)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap());
|
||||
state.set(obj, 2);
|
||||
expect(state.has(obj)).toBe(true);
|
||||
expect(state.get(obj)).toBe(2);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap([[obj, 2]]));
|
||||
state.delete(obj);
|
||||
expect(state.has(obj)).toBe(false);
|
||||
expect(state.get(obj)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("act like a WeakMap", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap([[obj, 2]]));
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(WeakMap);
|
||||
});
|
||||
|
||||
test("checking for a key with 'has' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const obj3 = {};
|
||||
const state = reactive(new WeakMap([[obj2, 2]]), observer);
|
||||
|
||||
expect(state.has(obj)).toBe(false); // subscribe to obj
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(obj, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(obj)).toBe(true); // subscribe to obj
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(obj, 3);
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
expect(state.has(obj)).toBe(false); // subscribe to obj
|
||||
|
||||
state.set(obj3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("checking for a key with 'get' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const obj3 = {};
|
||||
const state = reactive(new WeakMap([[obj2, 2]]), observer);
|
||||
|
||||
expect(state.get(obj)).toBeUndefined(); // subscribe to obj
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(obj, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.get(obj)).toBe(3); // subscribe to obj
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(obj, 3);
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
expect(state.get(obj)).toBeUndefined(); // subscribe to obj
|
||||
|
||||
state.set(obj3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("getting values returns a reactive", async () => {
|
||||
const keyObj = {};
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new WeakMap([[keyObj, valObj]]), observer);
|
||||
const reactiveObj = state.get(keyObj)!;
|
||||
expect(reactiveObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(valObj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const obj = reactive({ rawSet, rawWeakSet, rawMap, rawWeakMap });
|
||||
expect(obj.rawSet).toBe(rawSet);
|
||||
expect(obj.rawWeakSet).toBe(rawWeakSet);
|
||||
expect(obj.rawMap).toBe(rawMap);
|
||||
expect(obj.rawWeakMap).toBe(rawWeakMap);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1597,11 +1131,6 @@ describe("toRaw", () => {
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj);
|
||||
});
|
||||
|
||||
test("giving a non reactive to toRaw return the object itself", () => {
|
||||
const obj = { value: 1 };
|
||||
expect(toRaw(obj as Reactive<typeof obj>)).toBe(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reactivity: useState", () => {
|
||||
@@ -2123,7 +1652,7 @@ describe("Reactivity: useState", () => {
|
||||
expect([...steps]).toEqual(["list"]);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
|
||||
expect([...steps]).toEqual(["list"]);
|
||||
expect([...steps]).toEqual(["list", "quantity1"]);
|
||||
steps.clear();
|
||||
|
||||
secondQuantity.quantity = 2;
|
||||
|
||||
+2
-192
@@ -24,8 +24,8 @@ class Root extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// dev=false for benchmarking. we don't want to benchmark dev code!
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: false });`;
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });`;
|
||||
|
||||
const COMPONENTS_XML = /*xml*/`
|
||||
<templates>
|
||||
@@ -1573,191 +1573,6 @@ class Root extends Component {
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||
`;
|
||||
|
||||
const BENCHMARK_JS = `
|
||||
const { Component, mount, xml, useState, onWillRender, onPatched} = owl;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Data generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let idCounter = 1;
|
||||
const adjectives = [
|
||||
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
|
||||
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
|
||||
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
|
||||
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
|
||||
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
|
||||
|
||||
function _random (max) { return Math.round(Math.random() * 1000) % max; };
|
||||
|
||||
function buildData(count) {
|
||||
const data = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const label = \`\${adjectives[_random(adjectives.length)]} \${colours[_random(colours.length)]} \${nouns[_random(nouns.length)]}\`;
|
||||
data[i] = {
|
||||
id: idCounter++,
|
||||
label,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
class Button extends Component {
|
||||
static template = xml\`
|
||||
<div class='col-sm-6 smallpad'>
|
||||
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
|
||||
<t t-esc="props.text"/>
|
||||
</button>
|
||||
</div>\`;
|
||||
}
|
||||
|
||||
|
||||
class Row extends Component {
|
||||
static template = xml\`
|
||||
<tr t-att-class="props.isSelected ? 'danger' : ''">
|
||||
<td class="col-md-1" t-esc="props.row.id" />
|
||||
<td class="col-md-4">
|
||||
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
|
||||
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</a>
|
||||
</td>
|
||||
<td class='col-md-6'/>
|
||||
</tr>\`
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = xml\`
|
||||
<div class='container'>
|
||||
<div class='jumbotron'>
|
||||
<div class='row'>
|
||||
<div class='col-md-6'>
|
||||
<h1>Owl Keyed</h1>
|
||||
</div>
|
||||
<div class='col-md-6'>
|
||||
<div class='row'>
|
||||
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
|
||||
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
|
||||
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
|
||||
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
|
||||
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
|
||||
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-hover table-striped test-data'>
|
||||
<tbody>
|
||||
<t t-foreach="state.rows" t-as="row" t-key="row.id">
|
||||
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
|
||||
</div>\`;
|
||||
|
||||
static components = { Button, Row };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
rows: [],
|
||||
selectedRowId: null
|
||||
});
|
||||
this.benchmarking = false;
|
||||
onPatched(() => {
|
||||
if (this.benchmarking) {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start(descr) {
|
||||
this.benchmarking = \`[\${descr}]\`;
|
||||
console.time(this.benchmarking);
|
||||
}
|
||||
stop() {
|
||||
console.timeEnd(this.benchmarking);
|
||||
this.benchmarking = false;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.start('add1000');
|
||||
this.state.rows = buildData(1000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
runLots() {
|
||||
this.start('add10_000');
|
||||
this.state.rows = buildData(10_000);
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
add() {
|
||||
this.start('append1000');
|
||||
this.state.rows = this.state.rows.concat(buildData(1000));
|
||||
}
|
||||
|
||||
update() {
|
||||
this.start('update1/10th');
|
||||
let index = 0;
|
||||
const rows = this.state.rows;
|
||||
while (index < rows.length) {
|
||||
rows[index].label = rows[index].label + " !!!";
|
||||
index += 10;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.start('clear');
|
||||
this.state.rows = [];
|
||||
this.state.selectedRowId = null;
|
||||
}
|
||||
|
||||
swapRows() {
|
||||
this.start('swap');
|
||||
const rows = this.state.rows;
|
||||
if (rows.length > 998) {
|
||||
let tmp = rows[1];
|
||||
rows[1] = rows[998];
|
||||
rows[998] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
selectRow(id) {
|
||||
this.start('select');
|
||||
this.state.selectedRowId = id;
|
||||
}
|
||||
|
||||
removeRow(id) {
|
||||
this.start('remove1');
|
||||
const rows = this.state.rows;
|
||||
rows.splice(rows.findIndex(row => row.id === id), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||
|
||||
`;
|
||||
|
||||
const BENCHMARK_CSS = `
|
||||
tr.danger {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SAMPLES = [
|
||||
{
|
||||
description: "Components",
|
||||
@@ -1810,9 +1625,4 @@ export const SAMPLES = [
|
||||
xml: WMS_XML,
|
||||
css: WMS_CSS,
|
||||
},
|
||||
{
|
||||
description: "Benchmark example",
|
||||
code: BENCHMARK_JS,
|
||||
css: BENCHMARK_CSS,
|
||||
},
|
||||
];
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ async function startRelease() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
log(`Step 7/${STEPS}: Creating the release...`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${REL_NOTES_FILE}`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F release-notes.md`);
|
||||
if (relaseResult !== 0) {
|
||||
logError("github release failed. Aborting.");
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user