mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4fdd32f22 | |||
| 8d1d0a2244 | |||
| 0457e5d4ed | |||
| 8920b4b93a | |||
| 6908102a72 | |||
| d6348b8310 | |||
| 79738e00c7 | |||
| 2a1b99be2d | |||
| 8a472231cf | |||
| bb373e6a7a | |||
| d735213758 |
+5
-29
@@ -81,7 +81,6 @@ 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))
|
||||||
@@ -249,35 +248,12 @@ 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.
|
a much more powerful reactivity system, so the same rationale applies: in a way,
|
||||||
|
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
||||||
|
every value used by the component.
|
||||||
|
|
||||||
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||||
ideas may help:
|
as before.
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 🦉 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.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.0.0-alpha.3",
|
"version": "2.0.0-beta.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",
|
||||||
|
|||||||
+7
-3
@@ -18,6 +18,8 @@ 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";
|
||||||
|
|
||||||
@@ -46,11 +48,13 @@ export class App<
|
|||||||
if (config.test) {
|
if (config.test) {
|
||||||
this.dev = true;
|
this.dev = true;
|
||||||
}
|
}
|
||||||
if (this.dev && !config.test) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(DEV_MSG());
|
||||||
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
const env = config.env || {};
|
||||||
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
|
const descrs = Object.getOwnPropertyDescriptors(env);
|
||||||
|
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
|
||||||
this.props = config.props || ({} as P);
|
this.props = config.props || ({} as P);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UTILS = {
|
export const helpers = {
|
||||||
withDefault,
|
withDefault,
|
||||||
zero: Symbol("zero"),
|
zero: Symbol("zero"),
|
||||||
isBoundary,
|
isBoundary,
|
||||||
|
|||||||
+23
-23
@@ -1,12 +1,13 @@
|
|||||||
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 { UTILS } from "./template_helpers";
|
import { helpers } 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();
|
||||||
|
|
||||||
@@ -37,6 +38,22 @@ 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[];
|
||||||
@@ -50,13 +67,7 @@ export class TemplateSet {
|
|||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
utils: typeof UTILS = Object.assign({}, UTILS, {
|
helpers: any;
|
||||||
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;
|
||||||
@@ -65,6 +76,7 @@ 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(
|
||||||
@@ -108,7 +120,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.utils);
|
const template = templateFn(bdom, this.helpers);
|
||||||
this.templates[name] = template;
|
this.templates[name] = template;
|
||||||
}
|
}
|
||||||
return this.templates[name];
|
return this.templates[name];
|
||||||
@@ -123,15 +135,3 @@ 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;
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
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 } from "../reactivity";
|
import {
|
||||||
|
clearReactivesForCallback,
|
||||||
|
Reactive,
|
||||||
|
reactive,
|
||||||
|
TARGET,
|
||||||
|
NonReactive,
|
||||||
|
getSubscriptions,
|
||||||
|
} from "../reactivity";
|
||||||
import { batched, Callback } from "../utils";
|
import { batched, Callback } from "../utils";
|
||||||
import { Component, ComponentConstructor } from "./component";
|
import { Component, ComponentConstructor } from "./component";
|
||||||
import { fibersInError, handleError } from "./error_handling";
|
import { fibersInError, handleError } from "./error_handling";
|
||||||
@@ -51,6 +58,13 @@ export function useState<T extends object>(state: T): Reactive<T> | NonReactive<
|
|||||||
batchedRenderFunctions.set(node, render);
|
batchedRenderFunctions.set(node, render);
|
||||||
// manual implementation of onWillDestroy to break cyclic dependency
|
// manual implementation of onWillDestroy to break cyclic dependency
|
||||||
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||||
|
if (node.app.dev) {
|
||||||
|
Object.defineProperty(node, "subscriptions", {
|
||||||
|
get() {
|
||||||
|
return getSubscriptions(render);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return reactive(state, render);
|
return reactive(state, render);
|
||||||
}
|
}
|
||||||
@@ -93,8 +107,14 @@ export function component<P extends object>(
|
|||||||
|
|
||||||
const parentFiber = ctx.fiber!;
|
const parentFiber = ctx.fiber!;
|
||||||
if (node) {
|
if (node) {
|
||||||
|
let shouldRender = node.forceNextRender;
|
||||||
|
if (shouldRender) {
|
||||||
|
node.forceNextRender = false;
|
||||||
|
} else {
|
||||||
const currentProps = node.component.props[TARGET];
|
const currentProps = node.component.props[TARGET];
|
||||||
if (parentFiber.deep || arePropsDifferent(currentProps, props)) {
|
shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
|
||||||
|
}
|
||||||
|
if (shouldRender) {
|
||||||
node.updateAndRender(props, parentFiber);
|
node.updateAndRender(props, parentFiber);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -129,6 +149,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
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;
|
||||||
|
|||||||
@@ -43,7 +43,15 @@ 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);
|
||||||
|
|||||||
+1
-8
@@ -1,4 +1,3 @@
|
|||||||
import { UTILS } from "./app/template_helpers";
|
|
||||||
import {
|
import {
|
||||||
config,
|
config,
|
||||||
createBlock,
|
createBlock,
|
||||||
@@ -13,14 +12,10 @@ import {
|
|||||||
comment,
|
comment,
|
||||||
} from "./blockdom";
|
} from "./blockdom";
|
||||||
import { mainEventHandler } from "./component/handler";
|
import { mainEventHandler } from "./component/handler";
|
||||||
import { Portal } from "./portal";
|
|
||||||
import { markRaw } from "./reactivity";
|
|
||||||
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;
|
|
||||||
(UTILS as any).markRaw = markRaw;
|
|
||||||
|
|
||||||
export const blockDom = {
|
export const blockDom = {
|
||||||
config,
|
config,
|
||||||
@@ -42,11 +37,9 @@ 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, useState } from "./component/component_node";
|
||||||
export { status } from "./component/status";
|
export { status } from "./component/status";
|
||||||
export { Memo } from "./memo";
|
|
||||||
export { xml } from "./app/template_set";
|
|
||||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
export { 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 } from "./utils";
|
export { EventBus, whenReady, loadFile, markup, xml } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
|||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
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 "./app/template_set";
|
import { xml } from "./utils";
|
||||||
import { BDom, text, VNode } from "./blockdom";
|
import { BDom, text, VNode } from "./blockdom";
|
||||||
import { Component } from "./component/component";
|
import { Component } from "./component/component";
|
||||||
|
|
||||||
|
|||||||
+224
-22
@@ -7,9 +7,9 @@ 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;
|
||||||
@@ -18,8 +18,25 @@ export type Reactive<T extends Target = Target> = T & {
|
|||||||
export type NonReactive<T extends Target = Target> = T & {
|
export 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.
|
||||||
*
|
*
|
||||||
@@ -30,11 +47,17 @@ function canBeMadeReactive(value: any): boolean {
|
|||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// extract "RawType" from strings like "[object RawType]" => this lets us
|
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||||
// ignore many native objects such as Promise (whose toString is [object Promise])
|
}
|
||||||
// or Date ([object Date]).
|
/**
|
||||||
const rawType = objectToString.call(value).slice(8, -1);
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||||||
return rawType === "Object" || rawType === "Array";
|
* returns the original object otherwise.
|
||||||
|
*
|
||||||
|
* @param value the value make reactive
|
||||||
|
* @returns a reactive for the given object when possible, the original otherwise
|
||||||
|
*/
|
||||||
|
function possiblyReactive(val: any, cb: Callback) {
|
||||||
|
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +81,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<ObjectKey, Set<Callback>>>();
|
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, 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.
|
||||||
@@ -68,7 +91,7 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>
|
|||||||
* 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: ObjectKey, callback: Callback): void {
|
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||||
if (!targetToKeysToCallbacks.get(target)) {
|
if (!targetToKeysToCallbacks.get(target)) {
|
||||||
targetToKeysToCallbacks.set(target, new Map());
|
targetToKeysToCallbacks.set(target, new Map());
|
||||||
}
|
}
|
||||||
@@ -91,7 +114,7 @@ function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): v
|
|||||||
* @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: ObjectKey): void {
|
function notifyReactives(target: Target, key: PropertyKey): void {
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||||
if (!keyToCallbacks) {
|
if (!keyToCallbacks) {
|
||||||
return;
|
return;
|
||||||
@@ -130,6 +153,17 @@ 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
|
||||||
@@ -177,20 +211,32 @@ 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 proxy = new Proxy(target, {
|
const targetRawType = rawType(target);
|
||||||
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||||
|
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||||
|
: basicProxyHandler<T>(callback);
|
||||||
|
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||||
|
reactivesForTarget.set(callback, proxy);
|
||||||
|
}
|
||||||
|
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a basic proxy handler for regular objects and arrays.
|
||||||
|
*
|
||||||
|
* @param callback @see reactive
|
||||||
|
* @returns a proxy handler object
|
||||||
|
*/
|
||||||
|
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
||||||
|
return {
|
||||||
|
get(target: any, key: PropertyKey, proxy: Reactive<T>) {
|
||||||
if (key === TARGET) {
|
if (key === TARGET) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
observeTargetKey(target, key, callback);
|
observeTargetKey(target, key, callback);
|
||||||
const value = Reflect.get(target, key, proxy);
|
return possiblyReactive(Reflect.get(target, key, proxy), callback);
|
||||||
if (!canBeMadeReactive(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return reactive(value, callback);
|
|
||||||
},
|
},
|
||||||
set(target, key, value, proxy) {
|
set(target, key, value, proxy) {
|
||||||
const isNewKey = !Object.hasOwnProperty.call(target, key);
|
const isNewKey = !objectHasOwnProperty.call(target, key);
|
||||||
const originalValue = Reflect.get(target, key, proxy);
|
const originalValue = Reflect.get(target, key, proxy);
|
||||||
const ret = Reflect.set(target, key, value, proxy);
|
const ret = Reflect.set(target, key, value, proxy);
|
||||||
if (isNewKey) {
|
if (isNewKey) {
|
||||||
@@ -206,6 +252,7 @@ export function reactive<T extends Target>(
|
|||||||
},
|
},
|
||||||
deleteProperty(target, key) {
|
deleteProperty(target, key) {
|
||||||
const ret = Reflect.deleteProperty(target, key);
|
const ret = Reflect.deleteProperty(target, key);
|
||||||
|
// TODO: only notify when something was actually deleted
|
||||||
notifyReactives(target, KEYCHANGES);
|
notifyReactives(target, KEYCHANGES);
|
||||||
notifyReactives(target, key);
|
notifyReactives(target, key);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -216,11 +263,166 @@ export function reactive<T extends Target>(
|
|||||||
},
|
},
|
||||||
has(target, key) {
|
has(target, key) {
|
||||||
// TODO: this observes all key changes instead of only the presence of the argument 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);
|
observeTargetKey(target, KEYCHANGES, callback);
|
||||||
return Reflect.has(target, key);
|
return Reflect.has(target, key);
|
||||||
},
|
},
|
||||||
});
|
} as ProxyHandler<T>;
|
||||||
reactivesForTarget.set(callback, proxy);
|
|
||||||
}
|
}
|
||||||
return reactivesForTarget.get(callback) as Reactive<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,3 +71,17 @@ 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;
|
||||||
|
|||||||
@@ -1222,6 +1222,28 @@ 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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ 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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3189,6 +3189,71 @@ 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,5 +49,7 @@ 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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ 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 {
|
||||||
|
|||||||
+4
-3
@@ -15,10 +15,11 @@ import {
|
|||||||
useComponent,
|
useComponent,
|
||||||
xml,
|
xml,
|
||||||
} from "../src";
|
} from "../src";
|
||||||
import { UTILS } from "../src/app/template_helpers";
|
import { helpers } from "../src/app/template_helpers";
|
||||||
import { globalTemplates, TemplateSet } from "../src/app/template_set";
|
import { 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;
|
||||||
|
|
||||||
@@ -91,7 +92,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, UTILS)(context, node);
|
return fn(blockDom, helpers)(context, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderToString(template: string, context: any = {}, node?: any): string {
|
export function renderToString(template: string, context: any = {}, node?: any): string {
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// 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;
|
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
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: markRaw({'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;
|
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
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: markRaw({'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;
|
|
||||||
let { markRaw } = helpers;
|
|
||||||
|
|
||||||
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: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__1\`, node, ctx);
|
|
||||||
return multi([b2, b4]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+476
-10
@@ -171,6 +171,8 @@ 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++);
|
||||||
@@ -1093,18 +1095,482 @@ describe("Reactivity", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
expect(state.k).toEqual({ n: 2 });
|
expect(state.k).toEqual({ n: 2 });
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("can add collections set/weakset/map/weakmap in a reactive object", () => {
|
describe("Collections", () => {
|
||||||
const rawSet = new Set();
|
describe("Set", () => {
|
||||||
const rawWeakSet = new WeakSet();
|
test("can make reactive Set", () => {
|
||||||
const rawMap = new Map();
|
const set = new Set<number>();
|
||||||
const rawWeakMap = new WeakMap();
|
const obj = reactive(set);
|
||||||
|
expect(obj).not.toBe(set);
|
||||||
|
});
|
||||||
|
|
||||||
const obj = reactive({ rawSet, rawWeakSet, rawMap, rawWeakMap });
|
test("can read", async () => {
|
||||||
expect(obj.rawSet).toBe(rawSet);
|
const state = reactive(new Set([1]));
|
||||||
expect(obj.rawWeakSet).toBe(rawWeakSet);
|
expect(state.has(1)).toBe(true);
|
||||||
expect(obj.rawMap).toBe(rawMap);
|
expect(state.has(0)).toBe(false);
|
||||||
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+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 release-notes.md`);
|
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${REL_NOTES_FILE}`);
|
||||||
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