mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79a8139697 |
@@ -117,10 +117,3 @@ rawState.value = 3; // will NOT be picked up by the reactivity system!!!
|
||||
Here again, this is useful in some situations where we want to explicitely bypass
|
||||
Owl, but using this function means that the responsability of coordinating
|
||||
state update is given to the user code, instead of Owl. Subtle bugs may arise!
|
||||
|
||||
Also, normal (non-reactive objects) will be directly returned by `toRaw`:
|
||||
|
||||
```js
|
||||
const obj = { a: 1 };
|
||||
console.log(toRaw(obj) === obj); // true
|
||||
```
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
- [Fragments](#fragments)
|
||||
- [Inline templates](#inline-templates)
|
||||
- [Rendering svg](#rendering-svg)
|
||||
- [Restrictions](#restrictions)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -681,13 +680,3 @@ if a template is supposed to be included in a svg namespace or not. Therefore,
|
||||
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
|
||||
be considered as svg. In practice, this means that each component or each sub
|
||||
templates (included with `t-call`) should have one of these tag as root tag.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Note that Owl templates forbid the use of tag and or attributes starting with
|
||||
the `block-` string. This restriction prevents name collision with the internal
|
||||
code of Owl.
|
||||
|
||||
```xml
|
||||
<div><block-1>this will not be accepted by Owl</block-1></div>
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
||||
import { compile, Template } from "../compiler";
|
||||
import { component, getCurrent } from "../component/component_node";
|
||||
import { component } from "../component/component_node";
|
||||
import { UTILS } from "./template_helpers";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
||||
@@ -94,12 +94,7 @@ export class TemplateSet {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
const componentName = getCurrent().component.constructor.name;
|
||||
extraInfo = ` (for component "${componentName}")`;
|
||||
} catch {}
|
||||
throw new Error(`Missing template: "${name}"${extraInfo}`);
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
const templateFn = this._compileTemplate(name, rawTemplate);
|
||||
// first add a function to lazily get the template, in case there is a
|
||||
|
||||
@@ -525,6 +525,54 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
this.parentEl = parent;
|
||||
}
|
||||
patch(other: Block, withBeforeRemove: boolean) {}
|
||||
|
||||
hydrate(parent: HTMLElement, el: HTMLElement) {
|
||||
this.parentEl = parent;
|
||||
this.el = el;
|
||||
const refs: Node[] = new Array(refN);
|
||||
this.refs = refs;
|
||||
refs[0] = el;
|
||||
for (let i = 0; i < colN; i++) {
|
||||
const w = collectors[i];
|
||||
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
|
||||
}
|
||||
|
||||
// applying data to all update points
|
||||
if (locN) {
|
||||
const data = this.data!;
|
||||
for (let i = 0; i < locN; i++) {
|
||||
const loc = locations[i];
|
||||
loc.setData.call(refs[loc.refIdx], data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// preparing all children
|
||||
if (childN) {
|
||||
const children = this.children;
|
||||
for (let i = 0; i < childN; i++) {
|
||||
const child = children![i];
|
||||
if (child) {
|
||||
const loc = childrenLocs[i];
|
||||
let target: HTMLElement;
|
||||
if (loc.afterRefIdx) {
|
||||
target = refs[loc.afterRefIdx] as HTMLElement;
|
||||
const afterNode = document.createTextNode("");
|
||||
target.parentElement!.insertBefore(afterNode, target.nextSibling);
|
||||
refs[loc.afterRefIdx!] = afterNode;
|
||||
} else {
|
||||
target = refs[loc.parentRefIdx].firstChild! as HTMLElement;
|
||||
}
|
||||
|
||||
// const target = (loc.afterRefIdx ? refs[loc.afterRefIdx] : null) as HTMLElement;
|
||||
// const afterNode = document.createTextNode("");
|
||||
// target.parentElement!.insertBefore(afterNode, target.nextSibling);
|
||||
// refs[loc.afterRefIdx!] = afterNode;
|
||||
child.isOnlyChild = loc.isOnlyChild;
|
||||
(child as any).hydrate(target.parentElement, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
|
||||
@@ -42,3 +42,7 @@ export function withKey(vnode: VNode, key: any) {
|
||||
vnode.key = key;
|
||||
return vnode;
|
||||
}
|
||||
|
||||
export function hydrate(vnode: VNode, target: HTMLElement) {
|
||||
(vnode as any).hydrate(target.parentElement, target);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ abstract class VSimpleNode {
|
||||
this.el = node;
|
||||
}
|
||||
|
||||
hydrate(parent: HTMLElement, elem: Node) {
|
||||
this.parentEl = parent;
|
||||
this.el = elem;
|
||||
// this.mountNode(elem, parent, elem.nextSibling);
|
||||
}
|
||||
|
||||
moveBefore(other: VText | null, afterNode: Node | null) {
|
||||
const target = other ? other.el! : afterNode;
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
|
||||
@@ -305,9 +305,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (tagName === "t" && !dynamicTag) {
|
||||
return null;
|
||||
}
|
||||
if (tagName.startsWith("block-")) {
|
||||
throw new Error(`Invalid tag name: '${tagName}'`);
|
||||
}
|
||||
ctx = Object.assign({}, ctx);
|
||||
if (tagName === "pre") {
|
||||
ctx.inPreTag = true;
|
||||
@@ -374,8 +371,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
ctx = Object.assign({}, ctx);
|
||||
ctx.tModelInfo = model;
|
||||
}
|
||||
} else if (attr.startsWith("block-")) {
|
||||
throw new Error(`Invalid attribute: '${attr}'`);
|
||||
} else if (attr !== "t-name") {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
|
||||
@@ -120,30 +120,20 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
||||
this.initiateRender(fiber);
|
||||
}
|
||||
|
||||
initiateRender(fiber: Fiber | MountFiber) {
|
||||
async initiateRender(fiber: Fiber | MountFiber) {
|
||||
this.fiber = fiber;
|
||||
if (this.mounted.length) {
|
||||
fiber.root!.mounted.push(fiber);
|
||||
}
|
||||
const component = this.component;
|
||||
const finish = () => {
|
||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||
this._render(fiber);
|
||||
}
|
||||
}
|
||||
let willStartResults;
|
||||
try {
|
||||
willStartResults = this.willStart.map((f) => f.call(component));
|
||||
await Promise.all(this.willStart.map((f) => f.call(component)));
|
||||
} catch (e) {
|
||||
handleError({ node: this, error: e });
|
||||
return;
|
||||
}
|
||||
if (willStartResults.some(result => result instanceof Promise)) {
|
||||
Promise.all(willStartResults)
|
||||
.then(finish)
|
||||
.catch(e => handleError({ node: this, error: e }));
|
||||
} else {
|
||||
finish();
|
||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||
this._render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,27 +153,25 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
||||
|
||||
const fiber = makeRootFiber(this);
|
||||
this.fiber = fiber;
|
||||
debugger;
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.status === STATUS.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
// We only want to actually render the component if the following two
|
||||
// conditions are true:
|
||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||
// * (current || !fiber.parent): if current is not null, this means that the
|
||||
// render function was called when a render was already occurring. In this
|
||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||
// rendered to complete the work. If current is null, we check that the
|
||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||
// a root fiber to a child fiber in the previous microtick, because it was
|
||||
// embedded in a rendering coming from above, so the fiber will be rendered
|
||||
// in the next microtick anyway, so we should not render it again.
|
||||
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||||
this._render(fiber);
|
||||
}
|
||||
});
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
await Promise.resolve();
|
||||
if (this.status === STATUS.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
// We only want to actually render the component if the following two
|
||||
// conditions are true:
|
||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||
// * (current || !fiber.parent): if current is not null, this means that the
|
||||
// render function was called when a render was already occurring. In this
|
||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||
// rendered to complete the work. If current is null, we check that the
|
||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||
// a root fiber to a child fiber in the previous microtick, because it was
|
||||
// embedded in a rendering coming from above, so the fiber will be rendered
|
||||
// in the next microtick anyway, so we should not render it again.
|
||||
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||||
this._render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
_render(fiber: Fiber | RootFiber) {
|
||||
@@ -219,32 +207,25 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
||||
this.status = STATUS.DESTROYED;
|
||||
}
|
||||
|
||||
updateAndRender(props: any, parentFiber: Fiber) {
|
||||
async updateAndRender(props: any, parentFiber: Fiber) {
|
||||
// update
|
||||
const fiber = makeChildFiber(this, parentFiber);
|
||||
this.fiber = fiber;
|
||||
const component = this.component;
|
||||
applyDefaultProps(props, component.constructor as any);
|
||||
|
||||
const finish = () => {
|
||||
if (fiber !== this.fiber) {
|
||||
return;
|
||||
}
|
||||
component.props = props;
|
||||
this._render(fiber);
|
||||
const parentRoot = parentFiber.root!;
|
||||
if (this.willPatch.length) {
|
||||
parentRoot.willPatch.push(fiber);
|
||||
}
|
||||
if (this.patched.length) {
|
||||
parentRoot.patched.push(fiber);
|
||||
}
|
||||
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
||||
await prom;
|
||||
if (fiber !== this.fiber) {
|
||||
return;
|
||||
}
|
||||
const willUpdatePropsResults = this.willUpdateProps.map((f) => f.call(component, props));
|
||||
if (willUpdatePropsResults.some(res => res instanceof Promise)) {
|
||||
Promise.all(willUpdatePropsResults).then(finish);
|
||||
} else {
|
||||
finish();
|
||||
component.props = props;
|
||||
this._render(fiber);
|
||||
const parentRoot = parentFiber.root!;
|
||||
if (this.willPatch.length) {
|
||||
parentRoot.willPatch.push(fiber);
|
||||
}
|
||||
if (this.patched.length) {
|
||||
parentRoot.patched.push(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +272,15 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
hydrate(parent: HTMLElement, el: HTMLElement) {
|
||||
const bdom = this.fiber!.bdom!;
|
||||
this.bdom = bdom;
|
||||
(bdom as any).hydrate(parent, el);
|
||||
this.status = STATUS.MOUNTED;
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
|
||||
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
import { BDom, mount } from "../blockdom";
|
||||
import { BDom, hydrate, mount } from "../blockdom";
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import { fibersInError, handleError } from "./error_handling";
|
||||
import { STATUS } from "./status";
|
||||
@@ -6,7 +6,8 @@ import { STATUS } from "./status";
|
||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||
let current = node.fiber;
|
||||
if (current) {
|
||||
cancelFibers(current.children);
|
||||
let root = parent.root;
|
||||
cancelFibers(root, current.children);
|
||||
current.root = null;
|
||||
}
|
||||
return new Fiber(node, parent);
|
||||
@@ -16,7 +17,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
||||
let current = node.fiber;
|
||||
if (current) {
|
||||
let root = current.root!;
|
||||
root.counter -= cancelFibers(current.children);
|
||||
root.counter -= cancelFibers(root, current.children);
|
||||
current.children = [];
|
||||
root.counter++;
|
||||
current.bdom = null;
|
||||
@@ -41,14 +42,15 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
||||
/**
|
||||
* @returns number of not-yet rendered fibers cancelled
|
||||
*/
|
||||
function cancelFibers(fibers: Fiber[]): number {
|
||||
function cancelFibers(root: any, fibers: Fiber[]): number {
|
||||
let result = 0;
|
||||
for (let fiber of fibers) {
|
||||
fiber.node.fiber = null;
|
||||
fiber.root = root;
|
||||
if (!fiber.bdom) {
|
||||
result++;
|
||||
}
|
||||
result += cancelFibers(fiber.children);
|
||||
result += cancelFibers(root, fiber.children);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -142,15 +144,18 @@ type Position = "first-child" | "last-child";
|
||||
|
||||
export interface MountOptions {
|
||||
position?: Position;
|
||||
hydrate?: boolean;
|
||||
}
|
||||
|
||||
export class MountFiber extends RootFiber {
|
||||
target: HTMLElement;
|
||||
position: Position;
|
||||
hydrate?: boolean;
|
||||
|
||||
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
|
||||
super(node, null);
|
||||
this.target = target;
|
||||
this.hydrate = options.hydrate;
|
||||
this.position = options.position || "last-child";
|
||||
}
|
||||
complete() {
|
||||
@@ -166,7 +171,9 @@ export class MountFiber extends RootFiber {
|
||||
node.updateDom();
|
||||
} else {
|
||||
node.bdom = this.bdom;
|
||||
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||
if (this.hydrate) {
|
||||
hydrate(node.bdom!, this.target);
|
||||
} else if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||
mount(node.bdom!, this.target);
|
||||
} else {
|
||||
const firstChild = this.target.childNodes[0];
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export function markRaw<T extends Target>(value: T): NonReactive<T> {
|
||||
* @returns the underlying value
|
||||
*/
|
||||
export function toRaw<T extends object>(value: Reactive<T>): T {
|
||||
return value[TARGET] || value;
|
||||
return value[TARGET];
|
||||
}
|
||||
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { hydrate, patch, text, createBlock } from "../../src/blockdom";
|
||||
import { makeTestFixture } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("hydration", () => {
|
||||
test("simple text node", async () => {
|
||||
fixture.innerHTML = "some text";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const tree = text("some text");
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("some text");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, text("checkmate"));
|
||||
expect(fixture.innerHTML).toBe("checkmate");
|
||||
});
|
||||
|
||||
test("simple static block", async () => {
|
||||
fixture.innerHTML = "<div>some text</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block = createBlock("<div>some text</div>");
|
||||
|
||||
const tree = block();
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>some text</div>");
|
||||
expect(tree.el).toBe(target);
|
||||
});
|
||||
|
||||
test("simple dynamic block", async () => {
|
||||
fixture.innerHTML = "<div>some text</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block = createBlock("<div><block-text-0/></div>");
|
||||
|
||||
const tree = block(["some text"]);
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>some text</div>");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, block(["giuoco piano"]));
|
||||
expect(fixture.innerHTML).toBe("<div>giuoco piano</div>");
|
||||
});
|
||||
|
||||
test("block with sub block", async () => {
|
||||
fixture.innerHTML = "<div>queen<p>gambit</p></div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block1 = createBlock("<div><block-text-0/><block-child-0/></div>");
|
||||
const block2 = createBlock("<p><block-text-0/></p>");
|
||||
|
||||
const tree = block1(["queen"], [block2(["gambit"])]);
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>queen<p>gambit</p></div>");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, block1(["king"], [block2(["pawn"])]));
|
||||
expect(fixture.innerHTML).toBe("<div>king<p>pawn</p></div>");
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { renderToString } from "../helpers";
|
||||
|
||||
describe("blacklisted tags and attributes", () => {
|
||||
test("template with block-text tag", () => {
|
||||
const template = `<div><block-text-0/>hello</div>`;
|
||||
expect(() => renderToString(template)).toThrow("Invalid tag name: 'block-text-0'");
|
||||
});
|
||||
|
||||
test("template with block-handler tag", () => {
|
||||
const template = `<div block-handler-0="click">hello</div>`;
|
||||
expect(() => renderToString(template)).toThrow("Invalid attribute: 'block-handler-0'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`hydration can hydrate a component with a handler 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-handler-0=\\"click\\"><block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['inc'], ctx];
|
||||
let txt1 = ctx['state'].value;
|
||||
return block1([hdlr1, txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a component with a sub component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Counter\`, {}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a component with a sub component 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['inc'], ctx];
|
||||
let txt1 = ctx['state'].value;
|
||||
return block1([hdlr1, txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a simple static component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>giuoco pianissimo</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -204,7 +204,7 @@ describe("basics", () => {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe('Missing template: "wrongtemplate" (for component "Test")');
|
||||
expect(error!.message).toBe('Missing template: "wrongtemplate"');
|
||||
});
|
||||
|
||||
test("class component with dynamic text", async () => {
|
||||
|
||||
@@ -97,14 +97,17 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
||||
expect(["W:setup", "W:willStart", "W:willRender", "W:rendered", "W:mounted"]).toBeLogged();
|
||||
|
||||
expect(n).toBe(0);
|
||||
|
||||
w.state.val = 2;
|
||||
await new Promise(requestAnimationFrame); // calls to _render are batched per animation frame
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(n).toBe(1);
|
||||
|
||||
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
|
||||
|
||||
w.state.val = 3;
|
||||
await new Promise(requestAnimationFrame);
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(n).toBe(2);
|
||||
|
||||
expect([
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, mount, useState, xml } from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("hydration", () => {
|
||||
test("can hydrate a simple static component", async () => {
|
||||
fixture.innerHTML = "<div>giuoco pianissimo</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Test extends Component {
|
||||
static template = xml`<div>giuoco pianissimo</div>`;
|
||||
}
|
||||
|
||||
await mount(Test, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>giuoco pianissimo</div>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
});
|
||||
|
||||
test("can hydrate a component with a handler", async () => {
|
||||
fixture.innerHTML = "<div>0</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`<div t-on-click="inc"><t t-esc="state.value"/></div>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Counter, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
|
||||
target.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("can hydrate a component with a sub component", async () => {
|
||||
fixture.innerHTML = "<p><button>0</button></p>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`<button t-on-click="inc"><t t-esc="state.value"/></button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<p><Counter/></p>`;
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
await mount(Parent, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<p><button>0</button></p>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
|
||||
target.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<p><button>1</button></p>");
|
||||
});
|
||||
});
|
||||
@@ -1131,11 +1131,6 @@ describe("toRaw", () => {
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj);
|
||||
});
|
||||
|
||||
test("giving a non reactive to toRaw return the object itself", () => {
|
||||
const obj = { value: 1 };
|
||||
expect(toRaw(obj as Reactive<typeof obj>)).toBe(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reactivity: useState", () => {
|
||||
|
||||
Reference in New Issue
Block a user