Compare commits

..

2 Commits

Author SHA1 Message Date
Géry Debongnie e1fc513249 [IMP] big change: shallow render
With this commit, component only render child
components if they have different props (shallow
equality). Otherwise, we trust the reactivity
system to make sure that all impacted components
are updated
2022-01-21 09:48:53 +01:00
Géry Debongnie 73c37c025c [REF] move useState into component_node.ts 2022-01-21 09:48:53 +01:00
23 changed files with 640 additions and 158 deletions
+4 -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,8 @@ 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 nonReactiveProps = ctx.props && ctx.props[TARGET];
const slots = nonReactiveProps ? nonReactiveProps.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) {
+4 -8
View File
@@ -55,7 +55,7 @@ export class TemplateSet {
const template = this.getTemplate(subTemplate); const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key)); return toggler(subTemplate, template.call(owner, ctx, parent, key));
}, },
getTemplate: (name: string, nameSpace?: string) => this.getTemplate(name, nameSpace), getTemplate: (name: string) => this.getTemplate(name),
}); });
constructor(config: TemplateSetConfig = {}) { constructor(config: TemplateSetConfig = {}) {
@@ -87,15 +87,13 @@ export class TemplateSet {
} }
} }
getTemplate(name: string, nameSpace?: string): Template { getTemplate(name: string): Template {
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}"`); throw new Error(`Missing template: "${name}"`);
} }
console.log("getTemplate", nameSpace); const templateFn = this._compileTemplate(name, rawTemplate);
const templateFn = this._compileTemplate(name, rawTemplate, nameSpace);
// 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
// recursive call to the template name // recursive call to the template name
const templates = this.templates; const templates = this.templates;
@@ -108,14 +106,12 @@ export class TemplateSet {
return this.templates[name]; return this.templates[name];
} }
_compileTemplate(name: string, template: string | Node, nameSpace?: string) { _compileTemplate(name: string, template: string | Node) {
console.log("CompileTpm", nameSpace)
return compile(template, { return compile(template, {
name, name,
dev: this.dev, dev: this.dev,
translateFn: this.translateFn, translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes, translatableAttributes: this.translatableAttributes,
nameSpace: nameSpace,
}); });
} }
} }
+7 -19
View File
@@ -33,7 +33,6 @@ export interface Config {
export interface CodeGenOptions extends Config { export interface CodeGenOptions extends Config {
hasSafeContext?: boolean; hasSafeContext?: boolean;
name?: string; name?: string;
nameSpace?: string;
} }
// using a non-html document so that <inner/outer>HTML serializes as XML instead // using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -215,9 +214,8 @@ export class CodeGenerator {
translateFn: (s: string) => string; translateFn: (s: string) => string;
translatableAttributes: string[]; translatableAttributes: string[];
ast: AST; ast: AST;
staticCalls: { id: string; template: string, nameSpace?: string }[] = []; staticCalls: { id: string; template: string }[] = [];
helpers: Set<string> = new Set(); helpers: Set<string> = new Set();
nameSpace?: string;
constructor(ast: AST, options: CodeGenOptions) { constructor(ast: AST, options: CodeGenOptions) {
this.translateFn = options.translateFn || ((s: string) => s); this.translateFn = options.translateFn || ((s: string) => s);
@@ -226,8 +224,6 @@ export class CodeGenerator {
this.dev = options.dev || false; this.dev = options.dev || false;
this.ast = ast; this.ast = ast;
this.templateName = options.name; this.templateName = options.name;
this.nameSpace = options.nameSpace;
console.log('ThisNS', this.nameSpace)
} }
generateCode(): string { generateCode(): string {
@@ -242,7 +238,6 @@ export class CodeGenerator {
isLast: true, isLast: true,
translate: true, translate: true,
tKeyExpr: null, tKeyExpr: null,
nameSpace: this.nameSpace,
}); });
// define blocks and utility functions // define blocks and utility functions
let mainCode = [ let mainCode = [
@@ -255,12 +250,8 @@ export class CodeGenerator {
mainCode.push(`// Template name: "${this.templateName}"`); mainCode.push(`// Template name: "${this.templateName}"`);
} }
for (let { id, template, nameSpace } of this.staticCalls) { for (let { id, template } of this.staticCalls) {
let ns; mainCode.push(`const ${id} = getTemplate(${template});`);
if (nameSpace) {
ns = `, "${nameSpace}"`;
}
mainCode.push(`const ${id} = getTemplate(${template}${ns});`);
} }
// define all blocks // define all blocks
@@ -548,7 +539,6 @@ export class CodeGenerator {
// attributes // attributes
const attrs: { [key: string]: string } = {}; const attrs: { [key: string]: string } = {};
const nameSpace = ast.ns || ctx.nameSpace; const nameSpace = ast.ns || ctx.nameSpace;
console.log(nameSpace)
if (nameSpace && isNewBlock) { if (nameSpace && isNewBlock) {
// specific namespace uri // specific namespace uri
attrs["block-ns"] = nameSpace; attrs["block-ns"] = nameSpace;
@@ -970,7 +960,7 @@ export class CodeGenerator {
} else { } else {
const id = this.generateId(`callTemplate_`); const id = this.generateId(`callTemplate_`);
this.helpers.add("getTemplate"); this.helpers.add("getTemplate");
this.staticCalls.push({ id, template: subTemplate, nameSpace: ctx.nameSpace }); this.staticCalls.push({ id, template: subTemplate });
block = this.createBlock(block, "multi", ctx); block = this.createBlock(block, "multi", ctx);
this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block!, { this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block!, {
...ctx, ...ctx,
@@ -1091,11 +1081,9 @@ export class CodeGenerator {
let propString = propStr; let propString = propStr;
if (ast.dynamicProps) { if (ast.dynamicProps) {
if (!props.length) { propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
propString = `${compileExpr(ast.dynamicProps)}`; props.length ? ", " + propStr : ""
} else { })`;
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
} }
let propVar: string; let propVar: string;
-3
View File
@@ -8,7 +8,6 @@ export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config { interface CompileOptions extends Config {
name?: string; name?: string;
nameSpace?: string,
} }
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction { export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
// parsing // parsing
@@ -21,10 +20,8 @@ export function compile(template: string | Node, options: CompileOptions = {}):
: !template.includes("t-set") && !template.includes("t-call"); : !template.includes("t-set") && !template.includes("t-call");
// code generation // code generation
console.log("compile", options)
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext }); const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode(); const code = codeGenerator.generateCode();
console.log(code)
// template function // template function
return new Function("bdom, helpers", code) as TemplateFunction; return new Function("bdom, helpers", code) as TemplateFunction;
} }
+2 -2
View File
@@ -20,7 +20,7 @@ export class Component<Props = any, Env = any> {
setup() {} setup() {}
render() { render(force: boolean = false) {
this.__owl__.render(); this.__owl__.render(force);
} }
} }
+65 -6
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 } from "../reactivity";
import { batched, Callback } from "../utils";
import { Component } from "./component"; import { Component } 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";
@@ -23,6 +25,46 @@ export function useComponent(): Component {
return currentNode!.component; return currentNode!.component;
} }
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = currentNode!;
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node));
batchedRenderFunctions.set(node, render);
// manual implementation of onWillUnmount to break cyclic dependency
node.willUnmount.unshift(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 false;
}
export function component( export function component(
name: string | typeof Component, name: string | typeof Component,
props: any, props: any,
@@ -47,7 +89,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.force || arePropsDifferent(currentProps, props)) {
node.updateAndRender(props, parentFiber);
}
} else { } else {
// new component // new component
let C; let C;
@@ -69,7 +114,7 @@ export function component(
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Component VNode // Component VNode class
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type LifecycleHook = Function; type LifecycleHook = Function;
@@ -107,6 +152,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
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();
@@ -135,7 +181,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
} }
} }
async render() { async render(force: 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();
@@ -143,13 +189,15 @@ export class ComponentNode<T extends typeof Component = typeof Component>
current = this.fiber; current = this.fiber;
} }
if (current && !current.bdom && !fibersInError.has(current)) { if (current && !current.bdom && !fibersInError.has(current)) {
return; if (current.force || force === false) {
return;
}
} }
if (!this.bdom && !current) { if (!this.bdom && !current) {
return; return;
} }
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this, force);
this.fiber = fiber; this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
@@ -211,6 +259,9 @@ export class ComponentNode<T extends typeof Component = typeof Component>
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);
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) {
@@ -275,6 +326,14 @@ export class ComponentNode<T extends typeof Component = typeof Component>
} }
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) {
+6 -3
View File
@@ -13,7 +13,7 @@ export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
return new Fiber(node, parent); return new Fiber(node, parent);
} }
export function makeRootFiber(node: ComponentNode): Fiber { export function makeRootFiber(node: ComponentNode, force: boolean): Fiber {
let current = node.fiber; let current = node.fiber;
if (current) { if (current) {
let root = current.root!; let root = current.root!;
@@ -21,6 +21,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
current.children = []; current.children = [];
root.counter++; root.counter++;
current.bdom = null; current.bdom = null;
current.force = force;
if (fibersInError.has(current)) { if (fibersInError.has(current)) {
fibersInError.delete(current); fibersInError.delete(current);
fibersInError.delete(root); fibersInError.delete(root);
@@ -35,7 +36,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
if (node.patched.length) { if (node.patched.length) {
fiber.patched.push(fiber); fiber.patched.push(fiber);
} }
fiber.force = force;
return fiber; return fiber;
} }
@@ -62,11 +63,13 @@ export class Fiber {
parent: Fiber | null; parent: Fiber | null;
children: Fiber[] = []; children: Fiber[] = [];
appliedToDom = false; appliedToDom = false;
force: 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.force = parent.force;
const root = parent.root!; const root = parent.root!;
root.counter++; root.counter++;
this.root = root; this.root = root;
@@ -109,7 +112,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
+2 -1
View File
@@ -42,7 +42,8 @@ export { useComponent } 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 } from "./reactivity"; export { reactive } from "./reactivity";
export { useState } from "./component/component_node";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks"; export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils"; export { EventBus, whenReady, loadFile, markup } from "./utils";
export { export {
+3 -30
View File
@@ -1,9 +1,7 @@
import { onWillUnmount } 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");
// Special key to subscribe to, to be notified of key creation/deletion // Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes"); const KEYCHANGES = Symbol("Key changes");
@@ -85,7 +83,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;
@@ -190,28 +188,3 @@ export function reactive<T extends Target>(target: T, callback: Callback = () =>
} }
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> {
const node = getCurrent()!;
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillUnmount(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
return reactiveState;
}
@@ -52,35 +52,6 @@ exports[`properly support svg namespace to svg tags added even if already in svg
}" }"
`; `;
exports[`properly support svg svg namespace added to sub-blocks (t-call) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`path\`, \\"http://www.w3.org/2000/svg\\");
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`properly support svg svg namespace added to sub-blocks (t-call) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<path/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg svg namespace added to sub-blocks 1`] = ` exports[`properly support svg svg namespace added to sub-blocks 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
-22
View File
@@ -1,7 +1,5 @@
import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers"; import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers";
import { mount } from "../../src/blockdom"; import { mount } from "../../src/blockdom";
import { mount as mountComponent, Component } from "../../src/index"
// NB: check the snapshots to see where the SVG namespaces are added // NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything(); snapshotEverything();
@@ -54,24 +52,4 @@ describe("properly support svg", () => {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg"); expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
} }
}); });
test.only("svg namespace added to sub-blocks (t-call)", async () => {
const templates = `<t>
<t t-name="svg"><svg><t t-call="path" /></svg></t>
<t t-name="path"><path /></t>
</t>
`
const fixture = makeTestFixture();
class Svg extends Component {
static template = "svg";
}
await mountComponent(Svg, fixture, {templates})
const elems = fixture.querySelectorAll("svg, path");
expect(elems.length).toEqual(2);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
}); });
@@ -968,7 +968,7 @@ exports[`basics update props of component without concrete own node 1`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['childProps'].key; const tKey_1 = ctx['childProps'].key;
let b2 = toggler(tKey_1, component(\`Child\`, ctx['childProps'], tKey_1 + key + \`__1\`, node, ctx)); let b2 = toggler(tKey_1, component(\`Child\`, Object.assign({}, ctx['childProps']), tKey_1 + key + \`__1\`, node, ctx));
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -1145,7 +1145,7 @@ exports[`properly behave when destroyed/unmounted while rendering 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = component(\`SubChild\`, {}, key + \`__1\`, node, ctx); let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -499,7 +499,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);
} }
}" }"
`; `;
@@ -0,0 +1,163 @@
// 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 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);
}
}"
`;
@@ -8,7 +8,7 @@ exports[`t-props basic use 1`] = `
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(\`Child\`, ctx['some'].obj, key + \`__1\`, node, ctx); let b2 = component(\`Child\`, Object.assign({}, ctx['some'].obj), key + \`__1\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -65,7 +65,7 @@ exports[`t-props t-props only 1`] = `
let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`); let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx); let b2 = component(\`Comp\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
+1 -1
View File
@@ -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(this.props).not.toBe(p);
} }
} }
+26 -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...");
} }
}); });
@@ -521,7 +523,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 +1909,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 +2469,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 +2504,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 +2746,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 +2773,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 +2804,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 +2828,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();
+1 -1
View File
@@ -223,7 +223,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();
}); });
+346
View File
@@ -0,0 +1,346 @@
import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src";
import {
makeTestFixture,
snapshotEverything,
nextTick,
useLogLifecycle,
makeDeferred,
} 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("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("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 force=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();
});
+1 -1
View File
@@ -65,7 +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).toBe(props); expect(this.props).not.toBe(props);
} }
} }
class Parent extends Component { class Parent extends Component {
+1 -1
View File
@@ -1612,7 +1612,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;