Compare commits

..

13 Commits

Author SHA1 Message Date
Géry Debongnie 076b0d774e [REL] v2.0.0-alpha.3
# v2.0.0-alpha.3

A new release, with the fine grained reactivity applied to component props,
and changes in the way Owl render subcomponents: it can now skip patching and
or rendering child components if needed.
2022-02-25 10:57:25 +01:00
Géry Debongnie f405fe9323 [DOC] add information about reactivity 2022-02-25 10:32:25 +01:00
Géry Debongnie 1ae9d514b9 [IMP] component: use reactivity to allow shallow renderings
With this commit, component only render child
components if they have different props (shallow
equality). Otherwise, we trust the reactivity
system to make sure that all impacted components
are updated
2022-02-25 10:32:25 +01:00
Géry Debongnie 592d9a458e [REF] move useState into component_node.ts 2022-02-25 10:32:25 +01:00
Géry Debongnie 0dbd2bd463 [REF] component: slightly simplify fiber code 2022-02-18 16:21:34 +01:00
Géry Debongnie 8ec7a6f9bf [FIX] compiler: prevent block- attributes and tags 2022-02-16 17:32:59 +01:00
Géry Debongnie 12b8ce963e [IMP] reactivity: toRaw now works with non reactive objects 2022-02-16 09:35:36 +01:00
Géry Debongnie bb9d65e95b [IMP] app: better error message when missing template 2022-02-16 09:35:36 +01:00
Géry Debongnie 73c339fff1 [DOC] remove outdated event bus doc 2022-02-15 10:31:03 +01:00
Géry Debongnie 406be446a5 [FIX] app: display correct url in dev mode message
closes #850
2022-02-15 10:15:07 +01:00
Géry Debongnie bd2aa8a72f [MISC] playground: improve benchmarking code 2022-02-15 09:07:32 +01:00
Géry Debongnie 3536f41f00 [FIX] blockdom: undefined properties are treated as empty strings 2022-02-15 09:07:32 +01:00
Samuel Degueldre 804ad3c35e [IMP] component: improve errors when thrown from lifecycle hooks
Previously, a crash in a lifecycle hook for any reason would throw an
error whose stack trace started from the scheduler and contained only
the place where the hook was called by owl, but not the place where the
hook was registered by the user. This proved very difficult for users to
debug as they cannot really tell which component registered that hook.

This commit alleviates the issue by creating a new Error when the hook
is originally called, and wrapping the registered callback in a try
catch, throwing an error with the correct stack trace instead of the
error in the hook, and setting the error in the hook as the cause of
this synthetic error.
2022-02-14 13:54:17 +01:00
43 changed files with 1455 additions and 295 deletions
+26 -1
View File
@@ -31,6 +31,7 @@ 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))
@@ -798,4 +799,28 @@ Rationale: the `browser` object caused more trouble than it was worth. Also, it
seems like this should be done in user space, not at the framework level. 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.
+5 -1
View File
@@ -68,12 +68,16 @@ 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()`**: calling this method directly will cause a rerender. Note * **`render(deep[=false])`**: 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
View File
@@ -1,25 +0,0 @@
# 🦉 Event Bus 🦉
It is sometimes useful to use a `Bus` to communicate informations between various
parts of the code. Owl has a very simple bus class, which manages subscriptions,
triggering events, and callbacks.
```js
const bus = new owl.core.EventBus();
bus.on("some-event", null, function (...args) {
console.log(...args);
});
bus.trigger("some-event", 1, 2, 3);
// [1,2,3] will be logged to the console
```
Its API is:
| Method | Description |
| -------------------------------- | --------------------------------- |
| `on(eventType, owner, callback)` | add a listener |
| `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions |
+12
View File
@@ -25,6 +25,11 @@ 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:
@@ -117,3 +122,10 @@ rawState.value = 3; // will NOT be picked up by the reactivity system!!!
Here again, this is useful in some situations where we want to explicitely bypass Here again, this is useful in some situations where we want to explicitely bypass
Owl, but using this function means that the responsability of coordinating Owl, but using this function means that the responsability of coordinating
state update is given to the user code, instead of Owl. Subtle bugs may arise! state update is given to the user code, instead of Owl. Subtle bugs may arise!
Also, normal (non-reactive objects) will be directly returned by `toRaw`:
```js
const obj = { a: 1 };
console.log(toRaw(obj) === obj); // true
```
+11
View File
@@ -21,6 +21,7 @@
- [Fragments](#fragments) - [Fragments](#fragments)
- [Inline templates](#inline-templates) - [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg) - [Rendering svg](#rendering-svg)
- [Restrictions](#restrictions)
## Overview ## Overview
@@ -680,3 +681,13 @@ if a template is supposed to be included in a svg namespace or not. Therefore,
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
be considered as svg. In practice, this means that each component or each sub be considered as svg. In practice, this means that each component or each sub
templates (included with `t-call`) should have one of these tag as root tag. templates (included with `t-call`) should have one of these tag as root tag.
## Restrictions
Note that Owl templates forbid the use of tag and or attributes starting with
the `block-` string. This restriction prevents name collision with the internal
code of Owl.
```xml
<div><block-1>this will not be accepted by Owl</block-1></div>
```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-alpha.2", "version": "2.0.0-alpha.3",
"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",
+13 -5
View File
@@ -18,14 +18,18 @@ export interface AppConfig<P, E> extends TemplateSetConfig {
test?: boolean; test?: boolean;
} }
export const DEV_MSG = `Owl is running in 'dev' mode. export const DEV_MSG = () => {
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
return `Owl is running in 'dev' mode.
This is not suitable for production use. This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`; See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
export class App< export class App<
T extends abstract new (...args: any) => any = any, T extends abstract new (...args: any) => any = any,
P = any, P extends object = any,
E = any E = any
> extends TemplateSet { > extends TemplateSet {
static validateTarget = validateTarget; static validateTarget = validateTarget;
@@ -43,7 +47,7 @@ export class App<
this.dev = true; this.dev = true;
} }
if (this.dev && !config.test) { if (this.dev && !config.test) {
console.info(DEV_MSG); console.info(DEV_MSG());
} }
const descrs = Object.getOwnPropertyDescriptors(config.env || {}); const descrs = Object.getOwnPropertyDescriptors(config.env || {});
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E; this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
@@ -98,7 +102,11 @@ export class App<
} }
} }
export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>( export async function mount<
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 = {}
+3 -1
View File
@@ -2,6 +2,8 @@ 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.
@@ -21,7 +23,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 && ctx.props.slots) || {}; const slots = ctx.props[TARGET].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) {
+7 -2
View File
@@ -1,6 +1,6 @@
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 { component } from "../component/component_node"; import { component, getCurrent } from "../component/component_node";
import { UTILS } from "./template_helpers"; import { UTILS } from "./template_helpers";
const bdom = { text, createBlock, list, multi, html, toggler, component, comment }; const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
@@ -94,7 +94,12 @@ export class TemplateSet {
if (!(name in this.templates)) { if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name]; const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) { if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`); let extraInfo = "";
try {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
throw new Error(`Missing template: "${name}"${extraInfo}`);
} }
const templateFn = this._compileTemplate(name, rawTemplate); const templateFn = this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a // first add a function to lazily get the template, in case there is a
+1 -1
View File
@@ -139,7 +139,7 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
export function makePropSetter(name: string): Setter<HTMLElement> { export function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) { return function setProp(this: HTMLElement, value: any) {
(this as any)[name] = value; (this as any)[name] = value || "";
}; };
} }
+7 -7
View File
@@ -1125,18 +1125,17 @@ export class CodeGenerator {
} }
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
props.push(`slots: ${slotDef}`); this.helpers.add("markRaw");
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) {
if (!props.length) { propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`; props.length ? ", " + propStr : ""
} else { })`;
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
} }
let propVar: string; let propVar: string;
@@ -1147,7 +1146,8 @@ export class CodeGenerator {
} }
if (slotDef && (ast.dynamicProps || hasSlotsProp)) { if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
this.addLine(`${propVar!}.slots = Object.assign(${slotDef}, ${propVar!}.slots)`); this.helpers.add("markRaw");
this.addLine(`${propVar!}.slots = markRaw(Object.assign(${slotDef}, ${propVar!}.slots))`);
} }
// cmap key // cmap key
+5
View File
@@ -305,6 +305,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
if (tagName === "t" && !dynamicTag) { if (tagName === "t" && !dynamicTag) {
return null; return null;
} }
if (tagName.startsWith("block-")) {
throw new Error(`Invalid tag name: '${tagName}'`);
}
ctx = Object.assign({}, ctx); ctx = Object.assign({}, ctx);
if (tagName === "pre") { if (tagName === "pre") {
ctx.inPreTag = true; ctx.inPreTag = true;
@@ -371,6 +374,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ctx = Object.assign({}, ctx); ctx = Object.assign({}, ctx);
ctx.tModelInfo = model; ctx.tModelInfo = model;
} }
} else if (attr.startsWith("block-")) {
throw new Error(`Invalid attribute: '${attr}'`);
} else if (attr !== "t-name") { } else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) { if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`); throw new Error(`Unknown QWeb directive: '${attr}'`);
+2 -2
View File
@@ -36,7 +36,7 @@ export class Component<Props = any, Env = any> {
setup() {} setup() {}
render() { render(deep: boolean = false) {
this.__owl__.render(); this.__owl__.render(deep);
} }
} }
+81 -15
View File
@@ -1,6 +1,9 @@
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 { 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,
@@ -9,7 +12,6 @@ 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";
@@ -26,13 +28,54 @@ export function useComponent(): Component {
return currentNode!.component; return currentNode!.component;
} }
export function component( // -----------------------------------------------------------------------------
name: string | typeof Component, // Integration with reactivity system (useState)
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));
}
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 { ): ComponentNode<P> {
let node: any = ctx.children[key]; let node: any = ctx.children[key];
let isDynamic = typeof name !== "string"; let isDynamic = typeof name !== "string";
@@ -50,7 +93,10 @@ export function component(
const parentFiber = ctx.fiber!; const parentFiber = ctx.fiber!;
if (node) { if (node) {
node.updateAndRender(props, parentFiber); const currentProps = node.component.props[TARGET];
if (parentFiber.deep || arePropsDifferent(currentProps, props)) {
node.updateAndRender(props, parentFiber);
}
} else { } else {
// new component // new component
let C; let C;
@@ -65,19 +111,18 @@ export function component(
node = new ComponentNode(C, props, ctx.app, ctx); node = new ComponentNode(C, props, ctx.app, ctx);
ctx.children[key] = node; ctx.children[key] = node;
const fiber = makeChildFiber(node, parentFiber); node.initiateRender(new Fiber(node, parentFiber));
node.initiateRender(fiber);
} }
return node; return node;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Component VNode // Component VNode class
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type LifecycleHook = Function; type LifecycleHook = Function;
export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E>> { export class ComponentNode<P extends object = 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;
@@ -108,6 +153,7 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
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();
@@ -137,21 +183,29 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
} }
} }
async render() { async render(deep: boolean = false) {
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 && !current.bdom && !fibersInError.has(current)) { if (current) {
return; if (!current.bdom && !fibersInError.has(current)) {
} if (deep) {
if (!this.bdom && !current) { // we want the render from this point on to be with deep=true
current.deep = deep;
}
return;
}
// if current rendering was with deep=true, we want this one to be the same
deep = deep || current.deep;
} else if (!this.bdom) {
return; return;
} }
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
fiber.deep = deep;
this.fiber = fiber; this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
@@ -213,6 +267,10 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
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;
props = useState(props);
currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom; await prom;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
@@ -277,6 +335,14 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
} }
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) {
+7 -9
View File
@@ -6,8 +6,7 @@ import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber; let current = node.fiber;
if (current) { if (current) {
let root = parent.root; cancelFibers(current.children);
cancelFibers(root, current.children);
current.root = null; current.root = null;
} }
return new Fiber(node, parent); return new Fiber(node, parent);
@@ -17,9 +16,8 @@ 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 -= cancelFibers(root, current.children); root.counter = root.counter + 1 - 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);
@@ -35,22 +33,20 @@ 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;
} }
/** /**
* @returns number of not-yet rendered fibers cancelled * @returns number of not-yet rendered fibers cancelled
*/ */
function cancelFibers(root: any, fibers: Fiber[]): number { 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;
fiber.root = root;
if (!fiber.bdom) { if (!fiber.bdom) {
result++; result++;
} }
result += cancelFibers(root, fiber.children); result += cancelFibers(fiber.children);
} }
return result; return result;
} }
@@ -62,11 +58,13 @@ 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;
@@ -109,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
+46 -11
View File
@@ -1,62 +1,97 @@
import { getCurrent } from "./component_node"; import { getCurrent } from "./component_node";
import { nodeErrorHandlers } from "./error_handling"; import { nodeErrorHandlers } from "./error_handling";
function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
cause: any;
};
return (...args: any[]) => {
try {
const result = fn(...args);
if (result instanceof Promise) {
return result.catch((cause) => {
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
throw error;
});
}
return result;
} catch (cause) {
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
throw error;
}
};
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// hooks // hooks
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export function onWillStart(fn: () => Promise<void> | void | any) { export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent(); const node = getCurrent();
node.willStart.push(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
} }
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) { export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent(); const node = getCurrent();
node.willUpdateProps.push(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
} }
export function onMounted(fn: () => void | any) { export function onMounted(fn: () => void | any) {
const node = getCurrent(); const node = getCurrent();
node.mounted.push(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
} }
export function onWillPatch(fn: () => Promise<void> | any | void) { export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent(); const node = getCurrent();
node.willPatch.unshift(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
} }
export function onPatched(fn: () => void | any) { export function onPatched(fn: () => void | any) {
const node = getCurrent(); const node = getCurrent();
node.patched.push(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
} }
export function onWillUnmount(fn: () => Promise<void> | void | any) { export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent(); const node = getCurrent();
node.willUnmount.unshift(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
} }
export function onWillDestroy(fn: () => Promise<void> | void | any) { export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent(); const node = getCurrent();
node.willDestroy.push(fn.bind(node.component)); const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
} }
export function onWillRender(fn: () => void | any) { export function onWillRender(fn: () => void | any) {
const node = getCurrent(); const node = getCurrent();
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.renderFn = decorate(() => {
fn.call(node.component); fn.call(node.component);
return renderFn(); return renderFn();
}; }, "onWillRender");
} }
export function onRendered(fn: () => void | any) { export function onRendered(fn: () => void | any) {
const node = getCurrent(); const node = getCurrent();
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { const decorate = node.app.dev ? wrapError : (fn: any) => fn;
node.renderFn = decorate(() => {
const result = renderFn(); const result = renderFn();
fn.call(node.component); fn.call(node.component);
return result; return result;
}; }, "onRendered");
} }
type OnErrorCallback = (error: any) => void | any; type OnErrorCallback = (error: any) => void | any;
+4 -2
View File
@@ -14,11 +14,13 @@ import {
} from "./blockdom"; } from "./blockdom";
import { mainEventHandler } from "./component/handler"; import { mainEventHandler } from "./component/handler";
import { Portal } from "./portal"; 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).Portal = Portal;
(UTILS as any).markRaw = markRaw;
export const blockDom = { export const blockDom = {
config, config,
@@ -38,11 +40,11 @@ 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 } 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 { Memo } from "./memo";
export { xml } from "./app/template_set"; export { xml } from "./app/template_set";
export { useState, 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 } from "./utils";
export { export {
+5 -32
View File
@@ -1,9 +1,7 @@
import { onWillDestroy } from "./component/lifecycle_hooks"; import { Callback } from "./utils";
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)
const TARGET = Symbol("Target"); export 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
@@ -17,7 +15,7 @@ export type Reactive<T extends Target = Target> = T & {
[TARGET]: any; [TARGET]: any;
}; };
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;
@@ -57,7 +55,7 @@ export function markRaw<T extends Target>(value: T): NonReactive<T> {
* @returns the underlying value * @returns the underlying value
*/ */
export function toRaw<T extends object>(value: Reactive<T>): T { export function toRaw<T extends object>(value: Reactive<T>): T {
return value[TARGET]; return value[TARGET] || value;
} }
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>(); const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
@@ -115,7 +113,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
*/ */
function clearReactivesForCallback(callback: Callback): void { export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback); const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) { if (!targetsToClear) {
return; return;
@@ -226,28 +224,3 @@ export function reactive<T extends Target>(
} }
return reactivesForTarget.get(callback) as Reactive<T>; return reactivesForTarget.get(callback) as Reactive<T>;
} }
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();
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillDestroy(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
return reactiveState;
}
+9
View File
@@ -169,6 +169,15 @@ describe("properties", () => {
expect(input.value).toBe("potato"); expect(input.value).toBe("potato");
}); });
test("input with value attribute, and undefined given", () => {
const block = createBlock(`<input block-attribute-0="value"/>`);
const tree = block([undefined]);
mount(tree, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
});
test("input type=checkbox with checked attribute", () => { test("input type=checkbox with checked attribute", () => {
// render input with initial value // render input with initial value
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`); const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
+13
View File
@@ -0,0 +1,13 @@
import { renderToString } from "../helpers";
describe("blacklisted tags and attributes", () => {
test("template with block-text tag", () => {
const template = `<div><block-text-0/>hello</div>`;
expect(() => renderToString(template)).toThrow("Invalid tag name: 'block-text-0'");
});
test("template with block-handler tag", () => {
const template = `<div block-handler-0="click">hello</div>`;
expect(() => renderToString(template)).toThrow("Invalid attribute: 'block-handler-0'");
});
});
@@ -1067,6 +1067,33 @@ 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
) { ) {
@@ -1145,7 +1172,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\`, {}, key + \`__1\`, node, ctx); let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -100,6 +100,7 @@ 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>\`);
@@ -108,7 +109,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -152,6 +153,7 @@ 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>\`);
@@ -160,7 +162,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -190,6 +192,7 @@ 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>\`);
@@ -200,7 +203,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx); let b5 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -269,6 +272,7 @@ 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>\`);
@@ -277,7 +281,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -321,6 +325,7 @@ 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>\`);
@@ -331,7 +336,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
} }
return block1([], [b3]); return block1([], [b3]);
} }
@@ -465,6 +470,7 @@ 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>\`);
@@ -473,7 +479,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -516,6 +522,7 @@ 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>\`);
@@ -525,7 +532,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([txt1], [b3]); return block1([txt1], [b3]);
} }
}" }"
@@ -569,6 +576,7 @@ 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>\`);
@@ -577,7 +585,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -620,6 +628,7 @@ 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>\`);
@@ -630,7 +639,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx); let b5 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -733,7 +742,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, withKey } = helpers; let { prepareList, capture, markRaw, withKey } = helpers;
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
let Comp1 = ctx['cp'].Comp; let Comp1 = ctx['cp'].Comp;
@@ -748,7 +757,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: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1); c_block1[i1] = withKey(component(\`ErrorHandler\`, {onError: ()=>this.cleanUp(v1.id),slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, ctx), key1);
} }
return list(c_block1); return list(c_block1);
} }
@@ -808,7 +817,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, withKey } = helpers; let { prepareList, capture, markRaw, withKey } = helpers;
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
let Comp1 = ctx['elem'][1]; let Comp1 = ctx['elem'][1];
@@ -823,7 +832,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: {'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: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2__\${key1}\`, node, ctx), key1);
} }
return list(c_block1); return list(c_block1);
} }
@@ -873,6 +882,7 @@ 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>\`);
@@ -882,7 +892,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx); let b4 = component(\`ErrorBoundary\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
return block1([], [b2, b4]); return block1([], [b2, b4]);
} }
}" }"
@@ -1092,6 +1102,19 @@ exports[`errors and promises an error in mounted call will reject the mount prom
}" }"
`; `;
exports[`errors and promises an error in onMounted callback will have the component's setup in its stack trace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`errors and promises an error in patched call will reject the render promise 1`] = ` exports[`errors and promises an error in patched call will reject the render promise 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -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\`, {}, key + \`__1\`, node, ctx); return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
} }
}" }"
`; `;
@@ -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 } = helpers; let { capture, markRaw } = 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: {'footer': {__render: slot1, __ctx: ctx1}}}, key + \`__1\`, node, ctx); let b3 = component(\`Dialog\`, {slots: markRaw({'footer': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx);
return block1([txt1], [b3]); return block1([txt1], [b3]);
} }
}" }"
@@ -0,0 +1,212 @@
// 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
@@ -410,6 +410,24 @@ exports[`t-model directive on an textarea 1`] = `
}" }"
`; `;
exports[`t-model directive t-model on an input with an undefined value 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<input block-attribute-0=\\"value\\" block-handler-1=\\"input\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
return block1([attr1, hdlr1]);
}
}"
`;
exports[`t-model directive t-model on select with static options 1`] = ` exports[`t-model directive t-model on select with static options 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -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 } = helpers; let { isBoundary, withDefault, setContextValue, capture, markRaw } = 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: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1\`, node, ctx); let b2 = component(\`Childcomp\`, {slots: markRaw({'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 } = helpers; let { capture, isBoundary, withDefault, LazyValue, safeOutput, markRaw } = 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: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); return component(\`Child\`, {slots: markRaw({'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 } = helpers; let { capture, isBoundary, withDefault, LazyValue, safeOutput, markRaw } = 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: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); return component(\`Blorg\`, {slots: markRaw({'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 } = helpers; let { capture, isBoundary, withDefault, LazyValue, markRaw } = 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: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); return component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx1}})}, key + \`__2\`, node, ctx);
} }
}" }"
`; `;
+3 -3
View File
@@ -1,4 +1,4 @@
import { App, Component, mount, status, useState, xml } from "../../src"; import { App, Component, mount, status, toRaw, 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(this.props).toBe(p); expect(toRaw(this.props)).toBe(p);
} }
} }
@@ -204,7 +204,7 @@ describe("basics", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe('Missing template: "wrongtemplate"'); expect(error!.message).toBe('Missing template: "wrongtemplate" (for component "Test")');
}); });
test("class component with dynamic text", async () => { test("class component with dynamic text", async () => {
+78 -18
View File
@@ -39,6 +39,8 @@ 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...");
} }
}); });
@@ -131,6 +133,58 @@ 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();
@@ -521,7 +575,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
} }
class Child extends Component { class Child extends Component {
static template = xml`<div><SubChild /></div>`; static template = xml`<div><SubChild val="props.val"/></div>`;
static components = { SubChild }; static components = { SubChild };
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
@@ -1907,18 +1961,13 @@ 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",
@@ -2472,9 +2521,9 @@ test("two renderings initiated between willPatch and patched", async () => {
useLogLifecycle(); useLogLifecycle();
onMounted(() => { onMounted(() => {
this.mounted = "Mounted"; this.mounted = "Mounted";
parent.render(); parent.render(true);
}); });
onWillUnmount(() => parent.render()); onWillUnmount(() => parent.render(true));
} }
} }
@@ -2507,15 +2556,11 @@ 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([ expect(["Parent:willPatch", "Panel:willPatch", "Panel:patched", "Parent:patched"]).toBeLogged();
"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";
@@ -2753,12 +2798,20 @@ 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",
@@ -2772,20 +2825,23 @@ 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(); parent.render(true);
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",
@@ -2800,12 +2856,14 @@ 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(); parent.render(true);
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",
@@ -2822,12 +2880,14 @@ 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();
+38 -17
View File
@@ -82,8 +82,6 @@ describe("basics", () => {
}); });
test("display a nice error if it cannot find component (in dev mode)", async () => { test("display a nice error if it cannot find component (in dev mode)", async () => {
const info = console.info;
console.info = jest.fn(() => {}); // dev mode message
class SomeComponent extends Component {} class SomeComponent extends Component {}
class Parent extends Component { class Parent extends Component {
static template = xml`<SomeMispelledComponent />`; static template = xml`<SomeMispelledComponent />`;
@@ -91,7 +89,7 @@ describe("basics", () => {
} }
let error: Error; let error: Error;
try { try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { test: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
@@ -100,8 +98,6 @@ describe("basics", () => {
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
expect(console.info).toBeCalledTimes(1);
console.info = info;
}); });
test("simple catchError", async () => { test("simple catchError", async () => {
@@ -179,6 +175,30 @@ describe("errors and promises", () => {
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
test("an error in onMounted callback will have the component's setup in its stack trace", async () => {
class App extends Component {
static template = xml`<div>abc</div>`;
setup() {
onMounted(() => {
throw new Error("boom");
});
}
}
let error: Error;
try {
await mount(App, fixture, { test: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.stack).toContain("App.setup");
expect(error!.stack).toContain("error_handling.test.ts");
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
test("an error in willPatch call will reject the render promise", async () => { test("an error in willPatch call will reject the render promise", async () => {
class Root extends Component { class Root extends Component {
static template = xml`<div><t t-esc="val"/></div>`; static template = xml`<div><t t-esc="val"/></div>`;
@@ -191,13 +211,13 @@ describe("errors and promises", () => {
} }
} }
const root = await mount(Root, fixture); const root = await mount(Root, fixture, { test: true });
root.val = 4; root.val = 4;
let error: Error; let error: Error;
root.render(); root.render();
await nextTick(); await nextTick();
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("boom"); expect(error!.message).toBe(`The following error occurred in onWillPatch: "boom"`);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0);
}); });
@@ -214,13 +234,13 @@ describe("errors and promises", () => {
} }
} }
const root = await mount(Root, fixture); const root = await mount(Root, fixture, { test: true });
root.val = 4; root.val = 4;
let error: Error; let error: Error;
root.render(); root.render();
await nextTick(); await nextTick();
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("boom"); expect(error!.message).toBe(`The following error occurred in onPatched: "boom"`);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0);
}); });
@@ -296,7 +316,6 @@ describe("errors and promises", () => {
}); });
test("errors in mounted and in willUnmount", async () => { test("errors in mounted and in willUnmount", async () => {
expect.assertions(4);
class Example extends Component { class Example extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
val: any; val: any;
@@ -313,9 +332,11 @@ describe("errors and promises", () => {
} }
try { try {
await mount(Example, fixture); await mount(Example, fixture, { test: true });
} catch (e) { } catch (e) {
expect((e as Error).message).toBe("Error in mounted"); expect((e as Error).message).toBe(
`The following error occurred in onMounted: "Error in mounted"`
);
} }
// 1 additional error is logged because the destruction of the app causes // 1 additional error is logged because the destruction of the app causes
// the onWillUnmount hook to be called and to fail // the onWillUnmount hook to be called and to fail
@@ -385,14 +406,14 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: any = null; let e: Error;
try { try {
await mount(Root, fixture); await mount(Root, fixture, { test: true });
} catch (error) { } catch (error) {
e = error; e = error as Error;
} }
expect(e.message).toBe( expect(e!.message).toBe(
"No active component (a hook function should only be called in 'setup')" `The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
); );
}); });
+2 -2
View File
@@ -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(); component.render(true);
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(); component.render(true);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>brain maggot</div>"); expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
}); });
+3 -7
View File
@@ -849,8 +849,9 @@ describe("lifecycle hooks", () => {
class Parent extends Component { class Parent extends Component {
static template = xml` static template = xml`
<Child />`; <Child someValue="state.value" />`;
static components = { Child }; static components = { Child };
state = useState({ value: 1 });
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
} }
@@ -871,7 +872,7 @@ describe("lifecycle hooks", () => {
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
parent.render(); // to block child render parent.state.value++; // to block child render
await nextTick(); await nextTick();
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged(); expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
@@ -1008,20 +1009,15 @@ 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();
}); });
+1 -1
View File
@@ -12,7 +12,7 @@ let mockConsoleWarn: any;
beforeAll(() => { beforeAll(() => {
console.info = (message: any) => { console.info = (message: any) => {
if (message === DEV_MSG) { if (message === DEV_MSG()) {
return; return;
} }
info(message); info(message);
+442
View File
@@ -0,0 +1,442 @@
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();
});
+56
View File
@@ -1675,4 +1675,60 @@ 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]");
});
}); });
+13
View File
@@ -30,6 +30,19 @@ describe("t-model directive", () => {
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>"); expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
}); });
test("t-model on an input with an undefined value", async () => {
class SomeComponent extends Component {
static template = xml`<input t-model="state.text"/>`;
state = useState({ text: undefined });
}
await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<input>");
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
});
test("basic use, on an input with bracket expression", async () => { test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component { class SomeComponent extends Component {
static template = xml` static template = xml`
+2 -1
View File
@@ -53,7 +53,7 @@ describe("t-props", () => {
}); });
test("basic use", async () => { test("basic use", async () => {
expect.assertions(4); expect.assertions(5);
let props = { a: 1, b: 2 }; let props = { a: 1, b: 2 };
@@ -65,6 +65,7 @@ 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 {
+6 -3
View File
@@ -4,6 +4,7 @@ exports[`Memo if no prop change, prevent renderings from above 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;
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
let b6 = text(ctx['state'].a); let b6 = text(ctx['state'].a);
@@ -16,7 +17,7 @@ exports[`Memo if no prop change, prevent renderings from above 1`] = `
let b2 = text(ctx['state'].a); let b2 = text(ctx['state'].a);
let b3 = text(ctx['state'].b); let b3 = text(ctx['state'].b);
let b4 = text(ctx['state'].c); 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); 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]); return multi([b2, b3, b4, b9]);
} }
}" }"
@@ -26,6 +27,7 @@ exports[`Memo if no props, prevent renderings from above 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;
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {value: ctx['state'].value}, key + \`__2\`, node, ctx); return component(\`Child\`, {value: ctx['state'].value}, key + \`__2\`, node, ctx);
@@ -33,7 +35,7 @@ exports[`Memo if no props, prevent renderings from above 1`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx); 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); let b4 = component(\`Memo\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__3\`, node, ctx);
return multi([b2, b4]); return multi([b2, b4]);
} }
}" }"
@@ -54,6 +56,7 @@ exports[`Memo if no props, prevent renderings from above (work with simple html)
"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;
function slot1(ctx, node, key = \\"\\") { function slot1(ctx, node, key = \\"\\") {
return text(ctx['state'].value); return text(ctx['state'].value);
@@ -61,7 +64,7 @@ exports[`Memo if no props, prevent renderings from above (work with simple html)
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value); let b2 = text(ctx['state'].value);
let b4 = component(\`Memo\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); let b4 = component(\`Memo\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__1\`, node, ctx);
return multi([b2, b4]); return multi([b2, b4]);
} }
}" }"
+2 -1
View File
@@ -149,6 +149,7 @@ 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>\`);
@@ -157,7 +158,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: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx); let b3 = component(\`Child\`, {slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
+5 -1
View File
@@ -29,7 +29,7 @@ snapshotEverything();
beforeAll(() => { beforeAll(() => {
console.info = (message: any) => { console.info = (message: any) => {
if (message === DEV_MSG) { if (message === DEV_MSG()) {
return; return;
} }
info(message); info(message);
@@ -421,6 +421,7 @@ 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();
@@ -430,6 +431,7 @@ 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();
@@ -443,6 +445,7 @@ 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();
@@ -459,6 +462,7 @@ 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 () => {
+6 -1
View File
@@ -1131,6 +1131,11 @@ describe("toRaw", () => {
expect(reactiveObj).not.toBe(obj); expect(reactiveObj).not.toBe(obj);
expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj); expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj);
}); });
test("giving a non reactive to toRaw return the object itself", () => {
const obj = { value: 1 };
expect(toRaw(obj as Reactive<typeof obj>)).toBe(obj);
});
}); });
describe("Reactivity: useState", () => { describe("Reactivity: useState", () => {
@@ -1652,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", "quantity1"]); expect([...steps]).toEqual(["list"]);
steps.clear(); steps.clear();
secondQuantity.quantity = 2; secondQuantity.quantity = 2;
+2 -2
View File
@@ -24,8 +24,8 @@ class Root extends Component {
} }
} }
// Application setup // dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: true });`; mount(Root, document.body, { templates: TEMPLATES, dev: false });`;
const COMPONENTS_XML = /*xml*/` const COMPONENTS_XML = /*xml*/`
<templates> <templates>