mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aef6923c23 |
+29
-30
@@ -31,7 +31,6 @@ All changes are documented here in no particular order.
|
|||||||
- 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))
|
- 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))
|
- 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: 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))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +80,7 @@ All changes are documented here in no particular order.
|
|||||||
- improved performance
|
- improved performance
|
||||||
- much simpler code
|
- much simpler code
|
||||||
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
- 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))
|
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||||
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||||
@@ -248,12 +248,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),
|
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
|
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
|
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,
|
a much more powerful reactivity system.
|
||||||
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
|
||||||
every value used by the component.
|
|
||||||
|
|
||||||
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
||||||
as before.
|
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
|
### 9. component.el is removed
|
||||||
|
|
||||||
@@ -776,27 +799,3 @@ 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,
|
Migration: code should just be adapted to either use another browser object,
|
||||||
or to use native browser function (and then, just mock them directly).
|
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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ Are you new to Owl? This is the place to start!
|
|||||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
|
||||||
|
|
||||||
## Installing Owl
|
## Installing Owl
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -68,16 +68,12 @@ The `Component` class has a very small API.
|
|||||||
component will go through the following lifecycle methods: `willUpdateProps`,
|
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||||
`willPatch` and `patched`.
|
`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.
|
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
|
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
|
slightly later (at the next animation frame, if no component delays the
|
||||||
rendering)
|
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
|
## Static Properties
|
||||||
|
|
||||||
- **`template (string)`**: this is the name of the template that
|
- **`template (string)`**: this is the name of the template that
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ To solve this issue, Owl provides two reactivity primitives:
|
|||||||
|
|
||||||
Most of the time, the `useState` hook is the best solution.
|
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`
|
## `useState`
|
||||||
|
|
||||||
Let us start by an example of how `useState` could be used:
|
Let us start by an example of how `useState` could be used:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.0.0-alpha.2",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"browser": "dist/owl.iife.js",
|
"browser": "dist/owl.iife.js",
|
||||||
|
|||||||
+5
-13
@@ -18,8 +18,6 @@ export interface AppConfig<P, E> extends TemplateSetConfig {
|
|||||||
test?: boolean;
|
test?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasBeenLogged = false;
|
|
||||||
|
|
||||||
export const DEV_MSG = () => {
|
export const DEV_MSG = () => {
|
||||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
||||||
|
|
||||||
@@ -31,7 +29,7 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
|
|||||||
|
|
||||||
export class App<
|
export class App<
|
||||||
T extends abstract new (...args: any) => any = any,
|
T extends abstract new (...args: any) => any = any,
|
||||||
P extends object = any,
|
P = any,
|
||||||
E = any
|
E = any
|
||||||
> extends TemplateSet {
|
> extends TemplateSet {
|
||||||
static validateTarget = validateTarget;
|
static validateTarget = validateTarget;
|
||||||
@@ -48,13 +46,11 @@ export class App<
|
|||||||
if (config.test) {
|
if (config.test) {
|
||||||
this.dev = true;
|
this.dev = true;
|
||||||
}
|
}
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test) {
|
||||||
console.info(DEV_MSG());
|
console.info(DEV_MSG());
|
||||||
hasBeenLogged = true;
|
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
||||||
const descrs = Object.getOwnPropertyDescriptors(env);
|
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
|
||||||
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
|
|
||||||
this.props = config.props || ({} as P);
|
this.props = config.props || ({} as P);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +102,7 @@ export class App<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mount<
|
export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>(
|
||||||
T extends abstract new (...args: any) => any = any,
|
|
||||||
P extends object = any,
|
|
||||||
E = any
|
|
||||||
>(
|
|
||||||
C: T & ComponentConstructor<P, E>,
|
C: T & ComponentConstructor<P, E>,
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
config: AppConfig<P, E> & MountOptions = {}
|
config: AppConfig<P, E> & MountOptions = {}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { BDom, multi, text, toggler } from "../blockdom";
|
|||||||
import { validateProps } from "../component/props_validation";
|
import { validateProps } from "../component/props_validation";
|
||||||
import { Markup } from "../utils";
|
import { Markup } from "../utils";
|
||||||
import { html } from "../blockdom/index";
|
import { html } from "../blockdom/index";
|
||||||
import { TARGET } from "../reactivity";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains utility functions that will be injected in each template,
|
* This file contains utility functions that will be injected in each template,
|
||||||
* to perform various useful tasks in the compiled code.
|
* to perform various useful tasks in the compiled code.
|
||||||
@@ -23,7 +21,7 @@ function callSlot(
|
|||||||
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
||||||
): BDom {
|
): BDom {
|
||||||
key = key + "__slot_" + name;
|
key = key + "__slot_" + name;
|
||||||
const slots = ctx.props[TARGET].slots || {};
|
const slots = (ctx.props && ctx.props.slots) || {};
|
||||||
const { __render, __ctx, __scope } = slots[name] || {};
|
const { __render, __ctx, __scope } = slots[name] || {};
|
||||||
const slotScope = Object.create(__ctx || {});
|
const slotScope = Object.create(__ctx || {});
|
||||||
if (__scope) {
|
if (__scope) {
|
||||||
@@ -183,7 +181,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helpers = {
|
export const UTILS = {
|
||||||
withDefault,
|
withDefault,
|
||||||
zero: Symbol("zero"),
|
zero: Symbol("zero"),
|
||||||
isBoundary,
|
isBoundary,
|
||||||
|
|||||||
+23
-23
@@ -1,13 +1,12 @@
|
|||||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
||||||
import { compile, Template } from "../compiler";
|
import { compile, Template } from "../compiler";
|
||||||
import { markRaw } from "../reactivity";
|
|
||||||
import { Portal } from "../portal";
|
|
||||||
import { component, getCurrent } from "../component/component_node";
|
import { component, getCurrent } from "../component/component_node";
|
||||||
import { helpers } from "./template_helpers";
|
import { UTILS } from "./template_helpers";
|
||||||
import { globalTemplates } from "../utils";
|
|
||||||
|
|
||||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
||||||
|
|
||||||
|
export const globalTemplates: { [key: string]: string | Element } = {};
|
||||||
|
|
||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
||||||
@@ -38,22 +37,6 @@ function parseXML(xml: string): Document {
|
|||||||
return doc;
|
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 {
|
export interface TemplateSetConfig {
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
@@ -67,7 +50,13 @@ export class TemplateSet {
|
|||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string) => string;
|
||||||
translatableAttributes?: 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 = {}) {
|
constructor(config: TemplateSetConfig = {}) {
|
||||||
this.dev = config.dev || false;
|
this.dev = config.dev || false;
|
||||||
@@ -76,7 +65,6 @@ export class TemplateSet {
|
|||||||
if (config.templates) {
|
if (config.templates) {
|
||||||
this.addTemplates(config.templates);
|
this.addTemplates(config.templates);
|
||||||
}
|
}
|
||||||
this.helpers = makeHelpers(this.getTemplate.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addTemplate(
|
addTemplate(
|
||||||
@@ -120,7 +108,7 @@ export class TemplateSet {
|
|||||||
this.templates[name] = function (context, parent) {
|
this.templates[name] = function (context, parent) {
|
||||||
return templates[name].call(this, 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;
|
this.templates[name] = template;
|
||||||
}
|
}
|
||||||
return this.templates[name];
|
return this.templates[name];
|
||||||
@@ -135,3 +123,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;
|
||||||
|
|||||||
@@ -1125,17 +1125,18 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
|
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
|
||||||
this.helpers.add("markRaw");
|
props.push(`slots: ${slotDef}`);
|
||||||
props.push(`slots: markRaw(${slotDef})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const propStr = `{${props.join(",")}}`;
|
const propStr = `{${props.join(",")}}`;
|
||||||
|
|
||||||
let propString = propStr;
|
let propString = propStr;
|
||||||
if (ast.dynamicProps) {
|
if (ast.dynamicProps) {
|
||||||
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
|
if (!props.length) {
|
||||||
props.length ? ", " + propStr : ""
|
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
|
||||||
})`;
|
} else {
|
||||||
|
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let propVar: string;
|
let propVar: string;
|
||||||
@@ -1146,8 +1147,7 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
|
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
|
||||||
this.helpers.add("markRaw");
|
this.addLine(`${propVar!}.slots = Object.assign(${slotDef}, ${propVar!}.slots)`);
|
||||||
this.addLine(`${propVar!}.slots = markRaw(Object.assign(${slotDef}, ${propVar!}.slots))`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmap key
|
// cmap key
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Component<Props = any, Env = any> {
|
|||||||
|
|
||||||
setup() {}
|
setup() {}
|
||||||
|
|
||||||
render(deep: boolean = false) {
|
render() {
|
||||||
this.__owl__.render(deep);
|
this.__owl__.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-136
@@ -1,16 +1,6 @@
|
|||||||
import type { App, Env } from "../app/app";
|
import type { App, Env } from "../app/app";
|
||||||
import { BDom, VNode } from "../blockdom";
|
import { BDom, VNode } from "../blockdom";
|
||||||
import {
|
|
||||||
clearReactivesForCallback,
|
|
||||||
Reactive,
|
|
||||||
reactive,
|
|
||||||
TARGET,
|
|
||||||
NonReactive,
|
|
||||||
getSubscriptions,
|
|
||||||
} from "../reactivity";
|
|
||||||
import { batched, Callback } from "../utils";
|
|
||||||
import { Component, ComponentConstructor } from "./component";
|
import { Component, ComponentConstructor } from "./component";
|
||||||
import { fibersInError, handleError } from "./error_handling";
|
|
||||||
import {
|
import {
|
||||||
Fiber,
|
Fiber,
|
||||||
makeChildFiber,
|
makeChildFiber,
|
||||||
@@ -19,6 +9,7 @@ import {
|
|||||||
MountOptions,
|
MountOptions,
|
||||||
RootFiber,
|
RootFiber,
|
||||||
} from "./fibers";
|
} from "./fibers";
|
||||||
|
import { handleError, fibersInError } from "./error_handling";
|
||||||
import { applyDefaultProps } from "./props_validation";
|
import { applyDefaultProps } from "./props_validation";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
|
||||||
@@ -35,61 +26,13 @@ export function useComponent(): Component {
|
|||||||
return currentNode!.component;
|
return currentNode!.component;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
export function component(
|
||||||
// Integration with reactivity system (useState)
|
name: string | typeof Component,
|
||||||
// -----------------------------------------------------------------------------
|
props: any,
|
||||||
|
|
||||||
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));
|
|
||||||
if (node.app.dev) {
|
|
||||||
Object.defineProperty(node, "subscriptions", {
|
|
||||||
get() {
|
|
||||||
return getSubscriptions(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,
|
|
||||||
key: string,
|
key: string,
|
||||||
ctx: ComponentNode,
|
ctx: ComponentNode,
|
||||||
parent: any
|
parent: any
|
||||||
): ComponentNode<P> {
|
): ComponentNode {
|
||||||
let node: any = ctx.children[key];
|
let node: any = ctx.children[key];
|
||||||
let isDynamic = typeof name !== "string";
|
let isDynamic = typeof name !== "string";
|
||||||
|
|
||||||
@@ -107,16 +50,7 @@ export function component<P extends object>(
|
|||||||
|
|
||||||
const parentFiber = ctx.fiber!;
|
const parentFiber = ctx.fiber!;
|
||||||
if (node) {
|
if (node) {
|
||||||
let shouldRender = node.forceNextRender;
|
node.updateAndRender(props, parentFiber);
|
||||||
if (shouldRender) {
|
|
||||||
node.forceNextRender = false;
|
|
||||||
} else {
|
|
||||||
const currentProps = node.component.props[TARGET];
|
|
||||||
shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
|
|
||||||
}
|
|
||||||
if (shouldRender) {
|
|
||||||
node.updateAndRender(props, parentFiber);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// new component
|
// new component
|
||||||
let C;
|
let C;
|
||||||
@@ -131,25 +65,25 @@ export function component<P extends object>(
|
|||||||
node = new ComponentNode(C, props, ctx.app, ctx);
|
node = new ComponentNode(C, props, ctx.app, ctx);
|
||||||
ctx.children[key] = node;
|
ctx.children[key] = node;
|
||||||
|
|
||||||
node.initiateRender(new Fiber(node, parentFiber));
|
const fiber = makeChildFiber(node, parentFiber);
|
||||||
|
node.initiateRender(fiber);
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Component VNode class
|
// Component VNode
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
type LifecycleHook = Function;
|
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;
|
el?: HTMLElement | Text | undefined;
|
||||||
app: App;
|
app: App;
|
||||||
fiber: Fiber | null = null;
|
fiber: Fiber | null = null;
|
||||||
component: Component<P, E>;
|
component: Component<P, E>;
|
||||||
bdom: BDom | null = null;
|
bdom: BDom | null = null;
|
||||||
status: STATUS = STATUS.NEW;
|
status: STATUS = STATUS.NEW;
|
||||||
forceNextRender: boolean = false;
|
|
||||||
|
|
||||||
renderFn: Function;
|
renderFn: Function;
|
||||||
parent: ComponentNode | null;
|
parent: ComponentNode | null;
|
||||||
@@ -174,7 +108,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
applyDefaultProps(props, C);
|
applyDefaultProps(props, C);
|
||||||
const env = (parent && parent.childEnv) || app.env;
|
const env = (parent && parent.childEnv) || app.env;
|
||||||
this.childEnv = env;
|
this.childEnv = env;
|
||||||
props = useState(props);
|
|
||||||
this.component = new C(props, env, this) as any;
|
this.component = new C(props, env, this) as any;
|
||||||
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
||||||
this.component.setup();
|
this.component.setup();
|
||||||
@@ -187,66 +120,70 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
this.initiateRender(fiber);
|
this.initiateRender(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initiateRender(fiber: Fiber | MountFiber) {
|
initiateRender(fiber: Fiber | MountFiber) {
|
||||||
this.fiber = fiber;
|
this.fiber = fiber;
|
||||||
if (this.mounted.length) {
|
if (this.mounted.length) {
|
||||||
fiber.root!.mounted.push(fiber);
|
fiber.root!.mounted.push(fiber);
|
||||||
}
|
}
|
||||||
const component = this.component;
|
const component = this.component;
|
||||||
|
const finish = () => {
|
||||||
|
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||||
|
this._render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let willStartResults;
|
||||||
try {
|
try {
|
||||||
await Promise.all(this.willStart.map((f) => f.call(component)));
|
willStartResults = this.willStart.map((f) => f.call(component));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError({ node: this, error: e });
|
handleError({ node: this, error: e });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
if (willStartResults.some(result => result instanceof Promise)) {
|
||||||
this._render(fiber);
|
Promise.all(willStartResults)
|
||||||
|
.then(finish)
|
||||||
|
.catch(e => handleError({ node: this, error: e }));
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(deep: boolean = false) {
|
async render() {
|
||||||
let current = this.fiber;
|
let current = this.fiber;
|
||||||
if (current && current.root!.locked) {
|
if (current && current.root!.locked) {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
// situation may have changed after the microtask tick
|
// situation may have changed after the microtask tick
|
||||||
current = this.fiber;
|
current = this.fiber;
|
||||||
}
|
}
|
||||||
if (current) {
|
if (current && !current.bdom && !fibersInError.has(current)) {
|
||||||
if (!current.bdom && !fibersInError.has(current)) {
|
return;
|
||||||
if (deep) {
|
}
|
||||||
// we want the render from this point on to be with deep=true
|
if (!this.bdom && !current) {
|
||||||
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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fiber = makeRootFiber(this);
|
const fiber = makeRootFiber(this);
|
||||||
fiber.deep = deep;
|
|
||||||
this.fiber = fiber;
|
this.fiber = fiber;
|
||||||
|
debugger;
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (this.status === STATUS.DESTROYED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We only want to actually render the component if the following two
|
||||||
|
// conditions are true:
|
||||||
|
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||||
|
// * (current || !fiber.parent): if current is not null, this means that the
|
||||||
|
// render function was called when a render was already occurring. In this
|
||||||
|
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||||
|
// rendered to complete the work. If current is null, we check that the
|
||||||
|
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||||
|
// a root fiber to a child fiber in the previous microtick, because it was
|
||||||
|
// embedded in a rendering coming from above, so the fiber will be rendered
|
||||||
|
// in the next microtick anyway, so we should not render it again.
|
||||||
|
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||||||
|
this._render(fiber);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.app.scheduler.addFiber(fiber);
|
this.app.scheduler.addFiber(fiber);
|
||||||
await Promise.resolve();
|
|
||||||
if (this.status === STATUS.DESTROYED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We only want to actually render the component if the following two
|
|
||||||
// conditions are true:
|
|
||||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
|
||||||
// * (current || !fiber.parent): if current is not null, this means that the
|
|
||||||
// render function was called when a render was already occurring. In this
|
|
||||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
|
||||||
// rendered to complete the work. If current is null, we check that the
|
|
||||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
|
||||||
// a root fiber to a child fiber in the previous microtick, because it was
|
|
||||||
// embedded in a rendering coming from above, so the fiber will be rendered
|
|
||||||
// in the next microtick anyway, so we should not render it again.
|
|
||||||
if (this.fiber === fiber && (current || !fiber.parent)) {
|
|
||||||
this._render(fiber);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_render(fiber: Fiber | RootFiber) {
|
_render(fiber: Fiber | RootFiber) {
|
||||||
@@ -282,29 +219,32 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
this.status = STATUS.DESTROYED;
|
this.status = STATUS.DESTROYED;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAndRender(props: any, parentFiber: Fiber) {
|
updateAndRender(props: any, parentFiber: Fiber) {
|
||||||
// update
|
// update
|
||||||
const fiber = makeChildFiber(this, parentFiber);
|
const fiber = makeChildFiber(this, parentFiber);
|
||||||
this.fiber = fiber;
|
this.fiber = fiber;
|
||||||
const component = this.component;
|
const component = this.component;
|
||||||
applyDefaultProps(props, component.constructor as any);
|
applyDefaultProps(props, component.constructor as any);
|
||||||
|
|
||||||
currentNode = this;
|
const finish = () => {
|
||||||
props = useState(props);
|
if (fiber !== this.fiber) {
|
||||||
currentNode = null;
|
return;
|
||||||
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
}
|
||||||
await prom;
|
component.props = props;
|
||||||
if (fiber !== this.fiber) {
|
this._render(fiber);
|
||||||
return;
|
const parentRoot = parentFiber.root!;
|
||||||
|
if (this.willPatch.length) {
|
||||||
|
parentRoot.willPatch.push(fiber);
|
||||||
|
}
|
||||||
|
if (this.patched.length) {
|
||||||
|
parentRoot.patched.push(fiber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
component.props = props;
|
const willUpdatePropsResults = this.willUpdateProps.map((f) => f.call(component, props));
|
||||||
this._render(fiber);
|
if (willUpdatePropsResults.some(res => res instanceof Promise)) {
|
||||||
const parentRoot = parentFiber.root!;
|
Promise.all(willUpdatePropsResults).then(finish);
|
||||||
if (this.willPatch.length) {
|
} else {
|
||||||
parentRoot.willPatch.push(fiber);
|
finish();
|
||||||
}
|
|
||||||
if (this.patched.length) {
|
|
||||||
parentRoot.patched.push(fiber);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,14 +296,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
}
|
}
|
||||||
|
|
||||||
patch() {
|
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;
|
const hasChildren = Object.keys(this.children).length > 0;
|
||||||
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
|
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
|
|||||||
+5
-13
@@ -16,8 +16,9 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
let current = node.fiber;
|
let current = node.fiber;
|
||||||
if (current) {
|
if (current) {
|
||||||
let root = current.root!;
|
let root = current.root!;
|
||||||
root.counter = root.counter + 1 - cancelFibers(current.children);
|
root.counter -= cancelFibers(current.children);
|
||||||
current.children = [];
|
current.children = [];
|
||||||
|
root.counter++;
|
||||||
current.bdom = null;
|
current.bdom = null;
|
||||||
if (fibersInError.has(current)) {
|
if (fibersInError.has(current)) {
|
||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
@@ -33,6 +34,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
if (node.patched.length) {
|
if (node.patched.length) {
|
||||||
fiber.patched.push(fiber);
|
fiber.patched.push(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fiber;
|
return fiber;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +45,7 @@ function cancelFibers(fibers: Fiber[]): number {
|
|||||||
let result = 0;
|
let result = 0;
|
||||||
for (let fiber of fibers) {
|
for (let fiber of fibers) {
|
||||||
fiber.node.fiber = null;
|
fiber.node.fiber = null;
|
||||||
if (fiber.bdom) {
|
if (!fiber.bdom) {
|
||||||
// if fiber has been rendered, this means that the component props have
|
|
||||||
// been updated. however, this fiber will not be patched to the dom, so
|
|
||||||
// it could happen that the next render compare the current props with
|
|
||||||
// the same props, and skip the render completely. With the next line,
|
|
||||||
// we kindly request the component code to force a render, so it works as
|
|
||||||
// expected.
|
|
||||||
fiber.node.forceNextRender = true;
|
|
||||||
} else {
|
|
||||||
result++;
|
result++;
|
||||||
}
|
}
|
||||||
result += cancelFibers(fiber.children);
|
result += cancelFibers(fiber.children);
|
||||||
@@ -66,13 +60,11 @@ export class Fiber {
|
|||||||
parent: Fiber | null;
|
parent: Fiber | null;
|
||||||
children: Fiber[] = [];
|
children: Fiber[] = [];
|
||||||
appliedToDom = false;
|
appliedToDom = false;
|
||||||
deep: boolean = false;
|
|
||||||
|
|
||||||
constructor(node: ComponentNode, parent: Fiber | null) {
|
constructor(node: ComponentNode, parent: Fiber | null) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
this.deep = parent.deep;
|
|
||||||
const root = parent.root!;
|
const root = parent.root!;
|
||||||
root.counter++;
|
root.counter++;
|
||||||
this.root = root;
|
this.root = root;
|
||||||
@@ -115,7 +107,7 @@ export class RootFiber extends Fiber {
|
|||||||
current = undefined;
|
current = undefined;
|
||||||
|
|
||||||
// Step 2: patching the dom
|
// Step 2: patching the dom
|
||||||
node._patch();
|
node.patch();
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
|
|
||||||
// Step 4: calling all mounted lifecycle hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
|
|||||||
+8
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import { UTILS } from "./app/template_helpers";
|
||||||
import {
|
import {
|
||||||
config,
|
config,
|
||||||
createBlock,
|
createBlock,
|
||||||
@@ -12,10 +13,12 @@ import {
|
|||||||
comment,
|
comment,
|
||||||
} from "./blockdom";
|
} from "./blockdom";
|
||||||
import { mainEventHandler } from "./component/handler";
|
import { mainEventHandler } from "./component/handler";
|
||||||
|
import { Portal } from "./portal";
|
||||||
export type { Reactive } from "./reactivity";
|
export type { Reactive } from "./reactivity";
|
||||||
|
|
||||||
config.shouldNormalizeDom = false;
|
config.shouldNormalizeDom = false;
|
||||||
config.mainEventHandler = mainEventHandler;
|
config.mainEventHandler = mainEventHandler;
|
||||||
|
(UTILS as any).Portal = Portal;
|
||||||
|
|
||||||
export const blockDom = {
|
export const blockDom = {
|
||||||
config,
|
config,
|
||||||
@@ -35,11 +38,13 @@ export const blockDom = {
|
|||||||
|
|
||||||
export { App, mount } from "./app/app";
|
export { App, mount } from "./app/app";
|
||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { useComponent, useState } from "./component/component_node";
|
export { useComponent } from "./component/component_node";
|
||||||
export { status } from "./component/status";
|
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 { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
export { EventBus, whenReady, loadFile, markup, xml } from "./utils";
|
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
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 { onWillUnmount } from "./component/lifecycle_hooks";
|
||||||
import { xml } from "./utils";
|
import { xml } from "./app/template_set";
|
||||||
import { BDom, text, VNode } from "./blockdom";
|
import { BDom, text, VNode } from "./blockdom";
|
||||||
import { Component } from "./component/component";
|
import { Component } from "./component/component";
|
||||||
|
|
||||||
|
|||||||
+80
-255
@@ -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)
|
// 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
|
// Escape hatch to prevent reactivity system to turn something into a reactive
|
||||||
const SKIP = Symbol("Skip");
|
const SKIP = Symbol("Skip");
|
||||||
// Special key to subscribe to, to be notified of key creation/deletion
|
// Special key to subscribe to, to be notified of key creation/deletion
|
||||||
const KEYCHANGES = Symbol("Key changes");
|
const KEYCHANGES = Symbol("Key changes");
|
||||||
|
|
||||||
|
type ObjectKey = string | number | symbol;
|
||||||
|
|
||||||
type Target = object;
|
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 & {
|
export type Reactive<T extends Target = Target> = T & {
|
||||||
[TARGET]: any;
|
[TARGET]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NonReactive<T extends Target = Target> = T & {
|
type NonReactive<T extends Target = Target> = T & {
|
||||||
[SKIP]: any;
|
[SKIP]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const objectToString = Object.prototype.toString;
|
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.
|
* 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") {
|
if (typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
// 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]).
|
||||||
* Creates a reactive from the given object/callback if possible and returns it,
|
const rawType = objectToString.call(value).slice(8, -1);
|
||||||
* returns the original object otherwise.
|
return rawType === "Object" || rawType === "Array";
|
||||||
*
|
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +60,7 @@ export function toRaw<T extends object>(value: Reactive<T>): T {
|
|||||||
return value[TARGET] || value;
|
return value[TARGET] || value;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
* Observes a given key on a target with an callback. The callback will be
|
||||||
* called when the given key changes on the target.
|
* called when the given key changes on the target.
|
||||||
@@ -91,7 +70,7 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
|
|||||||
* or deletion)
|
* or deletion)
|
||||||
* @param callback the function to call when the key changes
|
* @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)) {
|
if (!targetToKeysToCallbacks.get(target)) {
|
||||||
targetToKeysToCallbacks.set(target, new Map());
|
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
|
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||||
* or deleted)
|
* or deleted)
|
||||||
*/
|
*/
|
||||||
function notifyReactives(target: Target, key: PropertyKey): void {
|
function notifyReactives(target: Target, key: ObjectKey): void {
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||||
if (!keyToCallbacks) {
|
if (!keyToCallbacks) {
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +115,7 @@ const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
|||||||
*
|
*
|
||||||
* @param callback the callback for which the reactives need to be cleared
|
* @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);
|
const targetsToClear = callbacksToTargets.get(callback);
|
||||||
if (!targetsToClear) {
|
if (!targetsToClear) {
|
||||||
return;
|
return;
|
||||||
@@ -153,17 +132,6 @@ export function clearReactivesForCallback(callback: Callback): void {
|
|||||||
targetsToClear.clear();
|
targetsToClear.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubscriptions(callback: Callback) {
|
|
||||||
const targets = callbacksToTargets.get(callback) || [];
|
|
||||||
return [...targets].map((target) => {
|
|
||||||
const keysToCallbacks = targetToKeysToCallbacks.get(target);
|
|
||||||
return {
|
|
||||||
target,
|
|
||||||
keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
|
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
|
||||||
/**
|
/**
|
||||||
* Creates a reactive proxy for an object. Reading data on the reactive object
|
* Creates a reactive proxy for an object. Reading data on the reactive object
|
||||||
@@ -211,218 +179,75 @@ export function reactive<T extends Target>(
|
|||||||
}
|
}
|
||||||
const reactivesForTarget = reactiveCache.get(target)!;
|
const reactivesForTarget = reactiveCache.get(target)!;
|
||||||
if (!reactivesForTarget.has(callback)) {
|
if (!reactivesForTarget.has(callback)) {
|
||||||
const targetRawType = rawType(target);
|
const proxy = new Proxy(target, {
|
||||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
||||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
if (key === TARGET) {
|
||||||
: basicProxyHandler<T>(callback);
|
return target;
|
||||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
}
|
||||||
|
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);
|
reactivesForTarget.set(callback, proxy);
|
||||||
}
|
}
|
||||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
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
|
* @param state the state to observe
|
||||||
* @returns a proxy handler object
|
* @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> {
|
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||||
return {
|
const node = getCurrent();
|
||||||
get(target: any, key: PropertyKey, proxy: Reactive<T>) {
|
if (!batchedRenderFunctions.has(node)) {
|
||||||
if (key === TARGET) {
|
batchedRenderFunctions.set(
|
||||||
return target;
|
node,
|
||||||
}
|
batched(() => node.render())
|
||||||
observeTargetKey(target, key, callback);
|
);
|
||||||
return possiblyReactive(Reflect.get(target, key, proxy), callback);
|
onWillDestroy(() => clearReactivesForCallback(render));
|
||||||
},
|
}
|
||||||
set(target, key, value, proxy) {
|
const render = batchedRenderFunctions.get(node)!;
|
||||||
const isNewKey = !objectHasOwnProperty.call(target, key);
|
const reactiveState = reactive(state, render);
|
||||||
const originalValue = Reflect.get(target, key, proxy);
|
return reactiveState;
|
||||||
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>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,17 +71,3 @@ export class Markup extends String {}
|
|||||||
export function markup(value: any) {
|
export function markup(value: any) {
|
||||||
return new Markup(value);
|
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;
|
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`destroying/recreating a subwidget with different props (if start is not over) 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"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>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b2]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -1222,28 +1195,6 @@ exports[`rendering component again in next microtick 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`rendering parent twice, with different props on child and stuff 1`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
return component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`rendering parent twice, with different props on child and stuff 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[`t-foreach with dynamic async component 1`] = `
|
exports[`t-foreach with dynamic async component 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ exports[`can catch errors can catch an error in a component render function 1`]
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ exports[`can catch errors can catch an error in a component render function 1`]
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -153,7 +152,6 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -162,7 +160,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -192,7 +190,6 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -203,7 +200,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b5]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -272,7 +269,6 @@ exports[`can catch errors can catch an error in the initial call of a component
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -281,7 +277,7 @@ exports[`can catch errors can catch an error in the initial call of a component
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -325,7 +321,6 @@ exports[`can catch errors can catch an error in the initial call of a component
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -336,7 +331,7 @@ exports[`can catch errors can catch an error in the initial call of a component
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b3;
|
let b3;
|
||||||
if (ctx['state'].flag) {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
@@ -470,7 +465,6 @@ exports[`can catch errors can catch an error in the mounted call 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -479,7 +473,7 @@ exports[`can catch errors can catch an error in the mounted call 1`] = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -522,7 +516,6 @@ exports[`can catch errors can catch an error in the willPatch call 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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>\`);
|
let block1 = createBlock(\`<div><span><block-text-0/></span><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -532,7 +525,7 @@ exports[`can catch errors can catch an error in the willPatch call 1`] = `
|
|||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let txt1 = ctx['state'].message;
|
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]);
|
return block1([txt1], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -576,7 +569,6 @@ exports[`can catch errors can catch an error in the willStart call 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -585,7 +577,7 @@ exports[`can catch errors can catch an error in the willStart call 1`] = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -628,7 +620,6 @@ exports[`can catch errors can catch an error origination from a child's willStar
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
@@ -639,7 +630,7 @@ exports[`can catch errors can catch an error origination from a child's willStar
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
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]);
|
return block1([], [b5]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -742,7 +733,7 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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 = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
let Comp1 = ctx['cp'].Comp;
|
let Comp1 = ctx['cp'].Comp;
|
||||||
@@ -757,7 +748,7 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
|
|||||||
let key1 = ctx['cp'].id;
|
let key1 = ctx['cp'].id;
|
||||||
const v1 = ctx['cp'];
|
const v1 = ctx['cp'];
|
||||||
const ctx1 = capture(ctx);
|
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);
|
return list(c_block1);
|
||||||
}
|
}
|
||||||
@@ -817,7 +808,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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 = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
let Comp1 = ctx['elem'][1];
|
let Comp1 = ctx['elem'][1];
|
||||||
@@ -832,7 +823,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
|
|||||||
let key1 = ctx['elem'][0];
|
let key1 = ctx['elem'][0];
|
||||||
const v1 = ctx['elem'];
|
const v1 = ctx['elem'];
|
||||||
const ctx1 = capture(ctx);
|
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);
|
return list(c_block1);
|
||||||
}
|
}
|
||||||
@@ -882,7 +873,6 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||||
|
|
||||||
@@ -892,7 +882,7 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
|||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let b2 = component(\`OK\`, {}, key + \`__1\`, node, ctx);
|
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]);
|
return block1([], [b2, b4]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ exports[`lifecycle hooks onWillRender 1`] = `
|
|||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
|
return component(\`Child\`, {}, 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`] = `
|
exports[`reactivity in lifecycle change state while mounting component 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ exports[`refs refs are properly bound in slots 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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 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>\`);
|
let block2 = createBlock(\`<button block-handler-0=\\"click\\" block-ref=\\"1\\">do something</button>\`);
|
||||||
@@ -77,7 +77,7 @@ exports[`refs refs are properly bound in slots 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
let txt1 = ctx['state'].val;
|
let txt1 = ctx['state'].val;
|
||||||
const ctx1 = capture(ctx);
|
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]);
|
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
@@ -4,7 +4,7 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 1`] = `
|
|||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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>\`);
|
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');
|
setContextValue(ctx, \\"iter\\", 'source');
|
||||||
let txt1 = ctx['iter'];
|
let txt1 = ctx['iter'];
|
||||||
const ctx1 = capture(ctx);
|
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'];
|
let txt2 = ctx['iter'];
|
||||||
return block1([txt1, txt2], [b2]);
|
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
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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 = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
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 = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
const ctx1 = capture(ctx);
|
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
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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>\`);
|
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 = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
const ctx1 = capture(ctx);
|
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
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
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 = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
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 = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
const ctx1 = capture(ctx);
|
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 { elem, makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
||||||
import { markup } from "../../src/utils";
|
import { markup } from "../../src/utils";
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ describe("basics", () => {
|
|||||||
class Test extends Component {
|
class Test extends Component {
|
||||||
static template = xml`<span>simple vnode</span>`;
|
static template = xml`<span>simple vnode</span>`;
|
||||||
setup() {
|
setup() {
|
||||||
expect(toRaw(this.props)).toBe(p);
|
expect(this.props).toBe(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ Scheduler.prototype.addFiber = function (fiber: Fiber) {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (lastScheduler && lastScheduler.tasks.size > 0) {
|
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...");
|
throw new Error("we got a memory leak...");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -99,17 +97,14 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
|||||||
expect(["W:setup", "W:willStart", "W:willRender", "W:rendered", "W:mounted"]).toBeLogged();
|
expect(["W:setup", "W:willStart", "W:willRender", "W:rendered", "W:mounted"]).toBeLogged();
|
||||||
|
|
||||||
expect(n).toBe(0);
|
expect(n).toBe(0);
|
||||||
|
|
||||||
w.state.val = 2;
|
w.state.val = 2;
|
||||||
await nextMicroTick();
|
await new Promise(requestAnimationFrame); // calls to _render are batched per animation frame
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
|
|
||||||
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
|
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
|
||||||
|
|
||||||
w.state.val = 3;
|
w.state.val = 3;
|
||||||
await nextMicroTick();
|
await new Promise(requestAnimationFrame);
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
|
|
||||||
expect([
|
expect([
|
||||||
@@ -133,58 +128,6 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
|||||||
]).toBeLogged();
|
]).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 () => {
|
test("creating two async components, scenario 1", async () => {
|
||||||
let defA = makeDeferred();
|
let defA = makeDeferred();
|
||||||
let defB = makeDeferred();
|
let defB = makeDeferred();
|
||||||
@@ -575,7 +518,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml`<div><SubChild val="props.val"/></div>`;
|
static template = xml`<div><SubChild /></div>`;
|
||||||
static components = { SubChild };
|
static components = { SubChild };
|
||||||
setup() {
|
setup() {
|
||||||
useLogLifecycle();
|
useLogLifecycle();
|
||||||
@@ -1961,13 +1904,18 @@ test("concurrent renderings scenario 13", async () => {
|
|||||||
await nextTick(); // wait for this change to be applied
|
await nextTick(); // wait for this change to be applied
|
||||||
expect([
|
expect([
|
||||||
"Parent:willRender",
|
"Parent:willRender",
|
||||||
|
"Child:willUpdateProps",
|
||||||
"Child:setup",
|
"Child:setup",
|
||||||
"Child:willStart",
|
"Child:willStart",
|
||||||
"Parent:rendered",
|
"Parent:rendered",
|
||||||
"Child:willRender",
|
"Child:willRender",
|
||||||
"Child:rendered",
|
"Child:rendered",
|
||||||
|
"Child:willRender",
|
||||||
|
"Child:rendered",
|
||||||
"Parent:willPatch",
|
"Parent:willPatch",
|
||||||
|
"Child:willPatch",
|
||||||
"Child:mounted",
|
"Child:mounted",
|
||||||
|
"Child:patched",
|
||||||
"Parent:patched",
|
"Parent:patched",
|
||||||
"Child:willRender",
|
"Child:willRender",
|
||||||
"Child:rendered",
|
"Child:rendered",
|
||||||
@@ -2521,9 +2469,9 @@ test("two renderings initiated between willPatch and patched", async () => {
|
|||||||
useLogLifecycle();
|
useLogLifecycle();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
this.mounted = "Mounted";
|
this.mounted = "Mounted";
|
||||||
parent.render(true);
|
parent.render();
|
||||||
});
|
});
|
||||||
onWillUnmount(() => parent.render(true));
|
onWillUnmount(() => parent.render());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2556,11 +2504,15 @@ test("two renderings initiated between willPatch and patched", async () => {
|
|||||||
"Parent:rendered",
|
"Parent:rendered",
|
||||||
]).toBeLogged();
|
]).toBeLogged();
|
||||||
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(["Panel:willRender", "Panel:rendered"]).toBeLogged();
|
|
||||||
|
|
||||||
await nextTick();
|
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>");
|
expect(fixture.innerHTML).toBe("<div><abc>Panel1Mounted</abc></div>");
|
||||||
|
|
||||||
parent.state.panel = "Panel2";
|
parent.state.panel = "Panel2";
|
||||||
@@ -2798,20 +2750,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
|||||||
static template = xml`<Parent state="state"/>`;
|
static template = xml`<Parent state="state"/>`;
|
||||||
static components = { Parent };
|
static components = { Parent };
|
||||||
state = { value: 0 };
|
state = { value: 0 };
|
||||||
setup() {
|
|
||||||
useLogLifecycle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = await mount(GrandParent, fixture);
|
const parent = await mount(GrandParent, fixture);
|
||||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||||
expect([
|
expect([
|
||||||
"GrandParent:setup",
|
|
||||||
"GrandParent:willStart",
|
|
||||||
"GrandParent:willRender",
|
|
||||||
"Parent:setup",
|
"Parent:setup",
|
||||||
"Parent:willStart",
|
"Parent:willStart",
|
||||||
"GrandParent:rendered",
|
|
||||||
"Parent:willRender",
|
"Parent:willRender",
|
||||||
"DelayedChild:setup",
|
"DelayedChild:setup",
|
||||||
"DelayedChild:willStart",
|
"DelayedChild:willStart",
|
||||||
@@ -2825,23 +2769,20 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
|||||||
"ReactiveChild:mounted",
|
"ReactiveChild:mounted",
|
||||||
"DelayedChild:mounted",
|
"DelayedChild:mounted",
|
||||||
"Parent:mounted",
|
"Parent:mounted",
|
||||||
"GrandParent:mounted",
|
|
||||||
]).toBeLogged();
|
]).toBeLogged();
|
||||||
|
|
||||||
promise = makeDeferred();
|
promise = makeDeferred();
|
||||||
const prom1 = promise;
|
const prom1 = promise;
|
||||||
parent.state.value = 1;
|
parent.state.value = 1;
|
||||||
child.render(); // trigger a root rendering first
|
child.render(); // trigger a root rendering first
|
||||||
parent.render(true);
|
parent.render();
|
||||||
reactiveChild.render();
|
reactiveChild.render();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||||
expect([
|
expect([
|
||||||
"DelayedChild:willRender",
|
"DelayedChild:willRender",
|
||||||
"DelayedChild:rendered",
|
"DelayedChild:rendered",
|
||||||
"GrandParent:willRender",
|
|
||||||
"Parent:willUpdateProps",
|
"Parent:willUpdateProps",
|
||||||
"GrandParent:rendered",
|
|
||||||
"ReactiveChild:willRender",
|
"ReactiveChild:willRender",
|
||||||
"ReactiveChild:rendered",
|
"ReactiveChild:rendered",
|
||||||
"Parent:willRender",
|
"Parent:willRender",
|
||||||
@@ -2856,14 +2797,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
|||||||
const prom2 = promise;
|
const prom2 = promise;
|
||||||
child.render(); // trigger a root rendering first
|
child.render(); // trigger a root rendering first
|
||||||
parent.state.value = 2;
|
parent.state.value = 2;
|
||||||
parent.render(true);
|
parent.render();
|
||||||
reactiveChild.render();
|
reactiveChild.render();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
||||||
expect([
|
expect([
|
||||||
"GrandParent:willRender",
|
|
||||||
"Parent:willUpdateProps",
|
"Parent:willUpdateProps",
|
||||||
"GrandParent:rendered",
|
|
||||||
"ReactiveChild:willRender",
|
"ReactiveChild:willRender",
|
||||||
"ReactiveChild:rendered",
|
"ReactiveChild:rendered",
|
||||||
"Parent:willRender",
|
"Parent:willRender",
|
||||||
@@ -2880,14 +2819,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
|
|||||||
expect([
|
expect([
|
||||||
"DelayedChild:willRender",
|
"DelayedChild:willRender",
|
||||||
"DelayedChild:rendered",
|
"DelayedChild:rendered",
|
||||||
"GrandParent:willPatch",
|
|
||||||
"Parent:willPatch",
|
"Parent:willPatch",
|
||||||
"ReactiveChild:willPatch",
|
"ReactiveChild:willPatch",
|
||||||
"DelayedChild:willPatch",
|
"DelayedChild:willPatch",
|
||||||
"DelayedChild:patched",
|
"DelayedChild:patched",
|
||||||
"ReactiveChild:patched",
|
"ReactiveChild:patched",
|
||||||
"Parent:patched",
|
"Parent:patched",
|
||||||
"GrandParent:patched",
|
|
||||||
]).toBeLogged();
|
]).toBeLogged();
|
||||||
|
|
||||||
prom1.resolve();
|
prom1.resolve();
|
||||||
@@ -3189,71 +3126,6 @@ test("Cascading renders after microtaskTick", async () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("0123 _ 0123");
|
expect(fixture.innerHTML).toBe("0123 _ 0123");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rendering parent twice, with different props on child and stuff", async () => {
|
|
||||||
class Child extends Component {
|
|
||||||
static template = xml`<t t-esc="props.value"/>`;
|
|
||||||
setup() {
|
|
||||||
useLogLifecycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Parent extends Component {
|
|
||||||
static template = xml`<Child value="state.value"/>`;
|
|
||||||
static components = { Child };
|
|
||||||
state = useState({ value: 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.value = 2;
|
|
||||||
// wait for child to be rendered
|
|
||||||
await nextMicroTick();
|
|
||||||
await nextMicroTick();
|
|
||||||
await nextMicroTick();
|
|
||||||
await nextMicroTick();
|
|
||||||
expect([
|
|
||||||
"Parent:willRender",
|
|
||||||
"Child:willUpdateProps",
|
|
||||||
"Parent:rendered",
|
|
||||||
"Child:willRender",
|
|
||||||
"Child:rendered",
|
|
||||||
]).toBeLogged();
|
|
||||||
expect(fixture.innerHTML).toBe("1");
|
|
||||||
|
|
||||||
// trigger a render, but keep the props for child the same
|
|
||||||
parent.render();
|
|
||||||
await nextTick();
|
|
||||||
expect(fixture.innerHTML).toBe("2");
|
|
||||||
expect([
|
|
||||||
"Parent:willRender",
|
|
||||||
"Child:willUpdateProps",
|
|
||||||
"Parent:rendered",
|
|
||||||
"Child:willRender",
|
|
||||||
"Child:rendered",
|
|
||||||
"Parent:willPatch",
|
|
||||||
"Child:willPatch",
|
|
||||||
"Child:patched",
|
|
||||||
"Parent:patched",
|
|
||||||
]).toBeLogged();
|
|
||||||
});
|
|
||||||
|
|
||||||
// test.skip("components with shouldUpdate=false", async () => {
|
// test.skip("components with shouldUpdate=false", async () => {
|
||||||
// const state = { p: 1, cc: 10 };
|
// const state = { p: 1, cc: 10 };
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,5 @@ describe("env handling", () => {
|
|||||||
|
|
||||||
await new App(Test, { env }).mount(fixture);
|
await new App(Test, { env }).mount(fixture);
|
||||||
expect(child.env).toEqual(env);
|
expect(child.env).toEqual(env);
|
||||||
// we check that the frozen env maintain the same prototype chain
|
|
||||||
expect(Object.getPrototypeOf(child.env)).toBe(Object.getPrototypeOf(env));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ describe("hooks", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
||||||
someVal = "brain";
|
someVal = "brain";
|
||||||
someVal2 = "maggot";
|
someVal2 = "maggot";
|
||||||
component.render(true);
|
component.render();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
||||||
});
|
});
|
||||||
@@ -272,7 +272,7 @@ describe("hooks", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
|
||||||
someVal = "brain";
|
someVal = "brain";
|
||||||
someVal2 = "maggot";
|
someVal2 = "maggot";
|
||||||
component.render(true);
|
component.render();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -849,9 +849,8 @@ describe("lifecycle hooks", () => {
|
|||||||
|
|
||||||
class Parent extends Component {
|
class Parent extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<Child someValue="state.value" />`;
|
<Child />`;
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state = useState({ value: 1 });
|
|
||||||
setup() {
|
setup() {
|
||||||
useLogLifecycle();
|
useLogLifecycle();
|
||||||
}
|
}
|
||||||
@@ -872,7 +871,7 @@ describe("lifecycle hooks", () => {
|
|||||||
"Parent:mounted",
|
"Parent:mounted",
|
||||||
]).toBeLogged();
|
]).toBeLogged();
|
||||||
|
|
||||||
parent.state.value++; // to block child render
|
parent.render(); // to block child render
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
|
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
|
||||||
|
|
||||||
@@ -1009,15 +1008,20 @@ describe("lifecycle hooks", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
expect([
|
expect([
|
||||||
"C:willRender",
|
"C:willRender",
|
||||||
|
"D:willUpdateProps",
|
||||||
"F:setup",
|
"F:setup",
|
||||||
"F:willStart",
|
"F:willStart",
|
||||||
"C:rendered",
|
"C:rendered",
|
||||||
|
"D:willRender",
|
||||||
|
"D:rendered",
|
||||||
"F:willRender",
|
"F:willRender",
|
||||||
"F:rendered",
|
"F:rendered",
|
||||||
"C:willPatch",
|
"C:willPatch",
|
||||||
|
"D:willPatch",
|
||||||
"E:willUnmount",
|
"E:willUnmount",
|
||||||
"E:willDestroy",
|
"E:willDestroy",
|
||||||
"F:mounted",
|
"F:mounted",
|
||||||
|
"D:patched",
|
||||||
"C:patched",
|
"C:patched",
|
||||||
]).toBeLogged();
|
]).toBeLogged();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,18 +53,6 @@ describe("reactivity in lifecycle", () => {
|
|||||||
expect(n).toBe(2); // no new rendering occured: b was never read via state!
|
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 () => {
|
test("state changes in willUnmount do not trigger rerender", async () => {
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -1675,60 +1675,4 @@ describe("slots", () => {
|
|||||||
await mount(Parent, fixture);
|
await mount(Parent, fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>SlotDisplay</div><div>Parent</div>");
|
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]");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe("t-props", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("basic use", async () => {
|
test("basic use", async () => {
|
||||||
expect.assertions(5);
|
expect.assertions(4);
|
||||||
|
|
||||||
let props = { a: 1, b: 2 };
|
let props = { a: 1, b: 2 };
|
||||||
|
|
||||||
@@ -65,7 +65,6 @@ describe("t-props", () => {
|
|||||||
`;
|
`;
|
||||||
setup() {
|
setup() {
|
||||||
expect(this.props).toEqual({ a: 1, b: 2 });
|
expect(this.props).toEqual({ a: 1, b: 2 });
|
||||||
expect(this.props).not.toBe(props);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Parent extends Component {
|
class Parent extends Component {
|
||||||
|
|||||||
+3
-4
@@ -15,11 +15,10 @@ import {
|
|||||||
useComponent,
|
useComponent,
|
||||||
xml,
|
xml,
|
||||||
} from "../src";
|
} from "../src";
|
||||||
import { helpers } from "../src/app/template_helpers";
|
import { UTILS } from "../src/app/template_helpers";
|
||||||
import { TemplateSet } from "../src/app/template_set";
|
import { globalTemplates, TemplateSet } from "../src/app/template_set";
|
||||||
import { BDom } from "../src/blockdom";
|
import { BDom } from "../src/blockdom";
|
||||||
import { compile } from "../src/compiler";
|
import { compile } from "../src/compiler";
|
||||||
import { globalTemplates } from "../src/utils";
|
|
||||||
|
|
||||||
const mount = blockDom.mount;
|
const mount = blockDom.mount;
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ export function renderToBdom(template: string, context: any = {}, node?: any): B
|
|||||||
snapshottedTemplates.add(template);
|
snapshottedTemplates.add(template);
|
||||||
expect(fn.toString()).toMatchSnapshot();
|
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 {
|
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
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
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 = \\"\\") {
|
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]);
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -421,7 +421,6 @@ describe("Portal", () => {
|
|||||||
addOutsideDiv(fixture);
|
addOutsideDiv(fixture);
|
||||||
const parent = await mount(Parent, fixture);
|
const parent = await mount(Parent, fixture);
|
||||||
expect(steps).toEqual(["parent:mounted"]);
|
expect(steps).toEqual(["parent:mounted"]);
|
||||||
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
||||||
|
|
||||||
parent.state.hasChild = true;
|
parent.state.hasChild = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -431,7 +430,6 @@ describe("Portal", () => {
|
|||||||
"child:mounted",
|
"child:mounted",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
]);
|
]);
|
||||||
expect(fixture.innerHTML).toBe('<div id="outside"><span>1</span></div><div></div>');
|
|
||||||
|
|
||||||
parent.state.val = 2;
|
parent.state.val = 2;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -445,7 +443,6 @@ describe("Portal", () => {
|
|||||||
"child:patched",
|
"child:patched",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
]);
|
]);
|
||||||
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
|
||||||
|
|
||||||
parent.state.hasChild = false;
|
parent.state.hasChild = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -462,7 +459,6 @@ describe("Portal", () => {
|
|||||||
"child:willUnmount",
|
"child:willUnmount",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
]);
|
]);
|
||||||
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("portal destroys on crash", async () => {
|
test("portal destroys on crash", async () => {
|
||||||
|
|||||||
+11
-477
@@ -171,8 +171,6 @@ describe("Reactivity", () => {
|
|||||||
expect(n).toBe(2);
|
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 () => {
|
test.skip("hasOwnProperty causes the key's presence to be observed", async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
const state = createReactive({}, () => n++);
|
const state = createReactive({}, () => n++);
|
||||||
@@ -1095,482 +1093,18 @@ describe("Reactivity", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
expect(state.k).toEqual({ n: 2 });
|
expect(state.k).toEqual({ n: 2 });
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("Collections", () => {
|
test("can add collections set/weakset/map/weakmap in a reactive object", () => {
|
||||||
describe("Set", () => {
|
const rawSet = new Set();
|
||||||
test("can make reactive Set", () => {
|
const rawWeakSet = new WeakSet();
|
||||||
const set = new Set<number>();
|
const rawMap = new Map();
|
||||||
const obj = reactive(set);
|
const rawWeakMap = new WeakMap();
|
||||||
expect(obj).not.toBe(set);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can read", async () => {
|
const obj = reactive({ rawSet, rawWeakSet, rawMap, rawWeakMap });
|
||||||
const state = reactive(new Set([1]));
|
expect(obj.rawSet).toBe(rawSet);
|
||||||
expect(state.has(1)).toBe(true);
|
expect(obj.rawWeakSet).toBe(rawWeakSet);
|
||||||
expect(state.has(0)).toBe(false);
|
expect(obj.rawMap).toBe(rawMap);
|
||||||
});
|
expect(obj.rawWeakMap).toBe(rawWeakMap);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2123,7 +1657,7 @@ describe("Reactivity: useState", () => {
|
|||||||
expect([...steps]).toEqual(["list"]);
|
expect([...steps]).toEqual(["list"]);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
|
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
|
||||||
expect([...steps]).toEqual(["list"]);
|
expect([...steps]).toEqual(["list", "quantity1"]);
|
||||||
steps.clear();
|
steps.clear();
|
||||||
|
|
||||||
secondQuantity.quantity = 2;
|
secondQuantity.quantity = 2;
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ async function startRelease() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
log(`Step 7/${STEPS}: Creating the release...`);
|
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) {
|
if (relaseResult !== 0) {
|
||||||
logError("github release failed. Aborting.");
|
logError("github release failed. Aborting.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user