Compare commits

..

2 Commits

Author SHA1 Message Date
Lucas Perais (lpe) 7a56cc6402 wiip 2022-01-21 17:33:41 +01:00
Lucas Perais (lpe) 1cadae86fd dd 2022-01-21 17:33:41 +01:00
23 changed files with 158 additions and 640 deletions
+1 -4
View File
@@ -2,8 +2,6 @@ import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
import { TARGET } from "../reactivity";
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
@@ -23,8 +21,7 @@ function callSlot(
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
key = key + "__slot_" + name;
const nonReactiveProps = ctx.props && ctx.props[TARGET];
const slots = nonReactiveProps ? nonReactiveProps.slots || {} : {};
const slots = (ctx.props && ctx.props.slots) || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = Object.create(__ctx || {});
if (__scope) {
+8 -4
View File
@@ -55,7 +55,7 @@ export class TemplateSet {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
},
getTemplate: (name: string) => this.getTemplate(name),
getTemplate: (name: string, nameSpace?: string) => this.getTemplate(name, nameSpace),
});
constructor(config: TemplateSetConfig = {}) {
@@ -87,13 +87,15 @@ export class TemplateSet {
}
}
getTemplate(name: string): Template {
getTemplate(name: string, nameSpace?: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`);
}
const templateFn = this._compileTemplate(name, rawTemplate);
console.log("getTemplate", nameSpace);
const templateFn = this._compileTemplate(name, rawTemplate, nameSpace);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
const templates = this.templates;
@@ -106,12 +108,14 @@ export class TemplateSet {
return this.templates[name];
}
_compileTemplate(name: string, template: string | Node) {
_compileTemplate(name: string, template: string | Node, nameSpace?: string) {
console.log("CompileTpm", nameSpace)
return compile(template, {
name,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
nameSpace: nameSpace,
});
}
}
+19 -7
View File
@@ -33,6 +33,7 @@ export interface Config {
export interface CodeGenOptions extends Config {
hasSafeContext?: boolean;
name?: string;
nameSpace?: string;
}
// using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -214,8 +215,9 @@ export class CodeGenerator {
translateFn: (s: string) => string;
translatableAttributes: string[];
ast: AST;
staticCalls: { id: string; template: string }[] = [];
staticCalls: { id: string; template: string, nameSpace?: string }[] = [];
helpers: Set<string> = new Set();
nameSpace?: string;
constructor(ast: AST, options: CodeGenOptions) {
this.translateFn = options.translateFn || ((s: string) => s);
@@ -224,6 +226,8 @@ export class CodeGenerator {
this.dev = options.dev || false;
this.ast = ast;
this.templateName = options.name;
this.nameSpace = options.nameSpace;
console.log('ThisNS', this.nameSpace)
}
generateCode(): string {
@@ -238,6 +242,7 @@ export class CodeGenerator {
isLast: true,
translate: true,
tKeyExpr: null,
nameSpace: this.nameSpace,
});
// define blocks and utility functions
let mainCode = [
@@ -250,8 +255,12 @@ export class CodeGenerator {
mainCode.push(`// Template name: "${this.templateName}"`);
}
for (let { id, template } of this.staticCalls) {
mainCode.push(`const ${id} = getTemplate(${template});`);
for (let { id, template, nameSpace } of this.staticCalls) {
let ns;
if (nameSpace) {
ns = `, "${nameSpace}"`;
}
mainCode.push(`const ${id} = getTemplate(${template}${ns});`);
}
// define all blocks
@@ -539,6 +548,7 @@ export class CodeGenerator {
// attributes
const attrs: { [key: string]: string } = {};
const nameSpace = ast.ns || ctx.nameSpace;
console.log(nameSpace)
if (nameSpace && isNewBlock) {
// specific namespace uri
attrs["block-ns"] = nameSpace;
@@ -960,7 +970,7 @@ export class CodeGenerator {
} else {
const id = this.generateId(`callTemplate_`);
this.helpers.add("getTemplate");
this.staticCalls.push({ id, template: subTemplate });
this.staticCalls.push({ id, template: subTemplate, nameSpace: ctx.nameSpace });
block = this.createBlock(block, "multi", ctx);
this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block!, {
...ctx,
@@ -1081,9 +1091,11 @@ export class CodeGenerator {
let propString = propStr;
if (ast.dynamicProps) {
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
props.length ? ", " + propStr : ""
})`;
if (!props.length) {
propString = `${compileExpr(ast.dynamicProps)}`;
} else {
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
}
let propVar: string;
+3
View File
@@ -8,6 +8,7 @@ export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config {
name?: string;
nameSpace?: string,
}
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
// parsing
@@ -20,8 +21,10 @@ export function compile(template: string | Node, options: CompileOptions = {}):
: !template.includes("t-set") && !template.includes("t-call");
// code generation
console.log("compile", options)
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
console.log(code)
// template function
return new Function("bdom, helpers", code) as TemplateFunction;
}
+2 -2
View File
@@ -20,7 +20,7 @@ export class Component<Props = any, Env = any> {
setup() {}
render(force: boolean = false) {
this.__owl__.render(force);
render() {
this.__owl__.render();
}
}
+6 -65
View File
@@ -1,9 +1,6 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { clearReactivesForCallback, Reactive, reactive, TARGET } from "../reactivity";
import { batched, Callback } from "../utils";
import { Component } from "./component";
import { fibersInError, handleError } from "./error_handling";
import {
Fiber,
makeChildFiber,
@@ -12,6 +9,7 @@ import {
MountOptions,
RootFiber,
} from "./fibers";
import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
@@ -25,46 +23,6 @@ export function useComponent(): Component {
return currentNode!.component;
}
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
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(
name: string | typeof Component,
props: any,
@@ -89,10 +47,7 @@ export function component(
const parentFiber = ctx.fiber!;
if (node) {
const currentProps = node.component.props[TARGET];
if (parentFiber.force || arePropsDifferent(currentProps, props)) {
node.updateAndRender(props, parentFiber);
}
node.updateAndRender(props, parentFiber);
} else {
// new component
let C;
@@ -114,7 +69,7 @@ export function component(
}
// -----------------------------------------------------------------------------
// Component VNode class
// Component VNode
// -----------------------------------------------------------------------------
type LifecycleHook = Function;
@@ -152,7 +107,6 @@ export class ComponentNode<T extends typeof Component = typeof Component>
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
props = useState(props);
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
@@ -181,7 +135,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
}
}
async render(force: boolean = false) {
async render() {
let current = this.fiber;
if (current && current.root!.locked) {
await Promise.resolve();
@@ -189,15 +143,13 @@ export class ComponentNode<T extends typeof Component = typeof Component>
current = this.fiber;
}
if (current && !current.bdom && !fibersInError.has(current)) {
if (current.force || force === false) {
return;
}
return;
}
if (!this.bdom && !current) {
return;
}
const fiber = makeRootFiber(this, force);
const fiber = makeRootFiber(this);
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
@@ -259,9 +211,6 @@ export class ComponentNode<T extends typeof Component = typeof Component>
this.fiber = fiber;
const component = this.component;
applyDefaultProps(props, component.constructor as any);
currentNode = this;
props = useState(props);
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
@@ -326,14 +275,6 @@ export class ComponentNode<T extends typeof Component = typeof Component>
}
patch() {
if (this.fiber && this.fiber.parent) {
// we only patch here renderings coming from above. renderings initiated
// by the component will be patched independently in the appropriate
// fiber.complete
this._patch();
}
}
_patch() {
const hasChildren = Object.keys(this.children).length > 0;
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
if (hasChildren) {
+3 -6
View File
@@ -13,7 +13,7 @@ export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode, force: boolean): Fiber {
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root!;
@@ -21,7 +21,6 @@ export function makeRootFiber(node: ComponentNode, force: boolean): Fiber {
current.children = [];
root.counter++;
current.bdom = null;
current.force = force;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
@@ -36,7 +35,7 @@ export function makeRootFiber(node: ComponentNode, force: boolean): Fiber {
if (node.patched.length) {
fiber.patched.push(fiber);
}
fiber.force = force;
return fiber;
}
@@ -63,13 +62,11 @@ export class Fiber {
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
force: boolean = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
this.parent = parent;
if (parent) {
this.force = parent.force;
const root = parent.root!;
root.counter++;
this.root = root;
@@ -112,7 +109,7 @@ export class RootFiber extends Fiber {
current = undefined;
// Step 2: patching the dom
node._patch();
node.patch();
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
+1 -2
View File
@@ -42,8 +42,7 @@ export { useComponent } from "./component/component_node";
export { status } from "./component/status";
export { Memo } from "./memo";
export { xml } from "./app/template_set";
export { reactive } from "./reactivity";
export { useState } from "./component/component_node";
export { useState, reactive } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
+30 -3
View File
@@ -1,7 +1,9 @@
import { Callback } from "./utils";
import { onWillUnmount } from "./component/lifecycle_hooks";
import { ComponentNode, getCurrent } from "./component/component_node";
import { batched, Callback } from "./utils";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
export const TARGET = Symbol("Target");
const TARGET = Symbol("Target");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
@@ -83,7 +85,7 @@ const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
*
* @param callback the callback for which the reactives need to be cleared
*/
export function clearReactivesForCallback(callback: Callback): void {
function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
@@ -188,3 +190,28 @@ export function reactive<T extends Target>(target: T, callback: Callback = () =>
}
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,6 +52,35 @@ 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`] = `
"function anonymous(bdom, helpers
) {
+22
View File
@@ -1,5 +1,7 @@
import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers";
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
snapshotEverything();
@@ -52,4 +54,24 @@ describe("properly support 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 = \\"\\") {
const tKey_1 = ctx['childProps'].key;
let b2 = toggler(tKey_1, component(\`Child\`, Object.assign({}, ctx['childProps']), tKey_1 + key + \`__1\`, node, ctx));
let b2 = toggler(tKey_1, component(\`Child\`, ctx['childProps'], tKey_1 + key + \`__1\`, node, ctx));
return block1([], [b2]);
}
}"
@@ -1145,7 +1145,7 @@ exports[`properly behave when destroyed/unmounted while rendering 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
let b2 = component(\`SubChild\`, {}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -499,7 +499,7 @@ exports[`lifecycle hooks onWillRender 1`] = `
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -1,163 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`force render in case of existing render 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`B\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`force render in case of existing render 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`C\`, {}, key + \`__1\`, node, ctx);
let b3 = text(ctx['props'].val);
return multi([b2, b3]);
}
}"
`;
exports[`force render in case of existing render 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`C\`);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics props are reactive (nested prop) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics props are reactive (nested prop) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].a.b.c);
}
}"
`;
exports[`rendering semantics props are reactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics props are reactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].a.b);
}
}"
`;
exports[`rendering semantics 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>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, Object.assign({}, ctx['some'].obj), key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, ctx['some'].obj, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -65,7 +65,7 @@ exports[`t-props t-props only 1`] = `
let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Comp\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
+1 -1
View File
@@ -121,7 +121,7 @@ describe("basics", () => {
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
setup() {
expect(this.props).not.toBe(p);
expect(this.props).toBe(p);
}
}
+18 -26
View File
@@ -39,8 +39,6 @@ Scheduler.prototype.addFiber = function (fiber: Fiber) {
afterEach(() => {
if (lastScheduler && lastScheduler.tasks.size > 0) {
// we still clear the scheduler to prevent additional noise
lastScheduler.tasks.clear();
throw new Error("we got a memory leak...");
}
});
@@ -523,7 +521,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
}
class Child extends Component {
static template = xml`<div><SubChild val="props.val"/></div>`;
static template = xml`<div><SubChild /></div>`;
static components = { SubChild };
setup() {
useLogLifecycle();
@@ -1909,13 +1907,18 @@ test("concurrent renderings scenario 13", async () => {
await nextTick(); // wait for this change to be applied
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:mounted",
"Child:patched",
"Parent:patched",
"Child:willRender",
"Child:rendered",
@@ -2469,9 +2472,9 @@ test("two renderings initiated between willPatch and patched", async () => {
useLogLifecycle();
onMounted(() => {
this.mounted = "Mounted";
parent.render(true);
parent.render();
});
onWillUnmount(() => parent.render(true));
onWillUnmount(() => parent.render());
}
}
@@ -2504,11 +2507,15 @@ test("two renderings initiated between willPatch and patched", async () => {
"Parent:rendered",
]).toBeLogged();
await nextMicroTick();
expect(["Panel:willRender", "Panel:rendered"]).toBeLogged();
await nextTick();
expect(["Parent:willPatch", "Panel:willPatch", "Panel:patched", "Parent:patched"]).toBeLogged();
expect([
"Panel:willRender",
"Panel:rendered",
"Parent:willPatch",
"Panel:willPatch",
"Panel:patched",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("<div><abc>Panel1Mounted</abc></div>");
parent.state.panel = "Panel2";
@@ -2746,20 +2753,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
static template = xml`<Parent state="state"/>`;
static components = { Parent };
state = { value: 0 };
setup() {
useLogLifecycle();
}
}
const parent = await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"GrandParent:setup",
"GrandParent:willStart",
"GrandParent:willRender",
"Parent:setup",
"Parent:willStart",
"GrandParent:rendered",
"Parent:willRender",
"DelayedChild:setup",
"DelayedChild:willStart",
@@ -2773,23 +2772,20 @@ test("delay willUpdateProps with rendering grandchild", async () => {
"ReactiveChild:mounted",
"DelayedChild:mounted",
"Parent:mounted",
"GrandParent:mounted",
]).toBeLogged();
promise = makeDeferred();
const prom1 = promise;
parent.state.value = 1;
child.render(); // trigger a root rendering first
parent.render(true);
parent.render();
reactiveChild.render();
await nextTick();
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
@@ -2804,14 +2800,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
const prom2 = promise;
child.render(); // trigger a root rendering first
parent.state.value = 2;
parent.render(true);
parent.render();
reactiveChild.render();
await nextTick();
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
@@ -2828,14 +2822,12 @@ test("delay willUpdateProps with rendering grandchild", async () => {
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willPatch",
"Parent:willPatch",
"ReactiveChild:willPatch",
"DelayedChild:willPatch",
"DelayedChild:patched",
"ReactiveChild:patched",
"Parent:patched",
"GrandParent:patched",
]).toBeLogged();
prom1.resolve();
+1 -1
View File
@@ -223,7 +223,7 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
someVal = "brain";
someVal2 = "maggot";
component.render(true);
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
});
+7 -3
View File
@@ -849,9 +849,8 @@ describe("lifecycle hooks", () => {
class Parent extends Component {
static template = xml`
<Child someValue="state.value" />`;
<Child />`;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
@@ -872,7 +871,7 @@ describe("lifecycle hooks", () => {
"Parent:mounted",
]).toBeLogged();
parent.state.value++; // to block child render
parent.render(); // to block child render
await nextTick();
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
@@ -1009,15 +1008,20 @@ describe("lifecycle hooks", () => {
await nextTick();
expect([
"C:willRender",
"D:willUpdateProps",
"F:setup",
"F:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"F:willRender",
"F:rendered",
"C:willPatch",
"D:willPatch",
"E:willUnmount",
"E:willDestroy",
"F:mounted",
"D:patched",
"C:patched",
]).toBeLogged();
});
-346
View File
@@ -1,346 +0,0 @@
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() {
expect(this.props).toEqual({ a: 1, b: 2 });
expect(this.props).not.toBe(props);
expect(this.props).toBe(props);
}
}
class Parent extends Component {
+1 -1
View File
@@ -1612,7 +1612,7 @@ describe("Reactivity: useState", () => {
expect([...steps]).toEqual(["list"]);
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
expect([...steps]).toEqual(["list"]);
expect([...steps]).toEqual(["list", "quantity1"]);
steps.clear();
secondQuantity.quantity = 2;