mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
prototype for hydration
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -272,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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -144,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() {
|
||||
@@ -168,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];
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -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>");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user