mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1fc513249 | |||
| 73c37c025c |
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
const currentProps = node.component.props[TARGET];
|
||||||
|
if (parentFiber.force || arePropsDifferent(currentProps, props)) {
|
||||||
node.updateAndRender(props, parentFiber);
|
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)) {
|
||||||
|
if (current.force || force === false) {
|
||||||
return;
|
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) {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user