mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] app: mount app in "first-child" position
We reintroduce the possibility to mount the app in first position in a target. The option "self" has been dropped since it is now possible for a component to have several top level nodes.
This commit is contained in:
committed by
Géry Debongnie
parent
06d207f60e
commit
7d7568d254
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from "./component/component";
|
import { Component } from "./component/component";
|
||||||
import { ComponentNode } from "./component/component_node";
|
import { ComponentNode } from "./component/component_node";
|
||||||
|
import { MountOptions } from "./component/fibers";
|
||||||
import { Scheduler } from "./component/scheduler";
|
import { Scheduler } from "./component/scheduler";
|
||||||
import { TemplateSet } from "./qweb/template_helpers";
|
import { TemplateSet } from "./qweb/template_helpers";
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mount(target: HTMLElement): Promise<InstanceType<T>> {
|
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
|
|||||||
}
|
}
|
||||||
const node = new ComponentNode(this.Root, this.props, this);
|
const node = new ComponentNode(this.Root, this.props, this);
|
||||||
this.root = node;
|
this.root = node;
|
||||||
return node.mountComponent(target);
|
return node.mountComponent(target, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export interface VNode<T = any> {
|
|||||||
|
|
||||||
export type BDom = VNode<any>;
|
export type BDom = VNode<any>;
|
||||||
|
|
||||||
export function mount(vnode: VNode, fixture: HTMLElement) {
|
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
|
||||||
vnode.mount(fixture, null);
|
vnode.mount(fixture, afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
|
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
makeChildFiber,
|
makeChildFiber,
|
||||||
makeRootFiber,
|
makeRootFiber,
|
||||||
MountFiber,
|
MountFiber,
|
||||||
|
MountOptions,
|
||||||
RootFiber,
|
RootFiber,
|
||||||
__internal__destroyed,
|
__internal__destroyed,
|
||||||
} from "./fibers";
|
} from "./fibers";
|
||||||
@@ -92,8 +93,8 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
|||||||
this.component.setup();
|
this.component.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
mountComponent(target: any): Promise<InstanceType<T>> {
|
mountComponent(target: any, options?: MountOptions): Promise<InstanceType<T>> {
|
||||||
const fiber = new MountFiber(this, target);
|
const fiber = new MountFiber(this, target, options);
|
||||||
this.app.scheduler.addFiber(fiber);
|
this.app.scheduler.addFiber(fiber);
|
||||||
this.initiateRender(fiber);
|
this.initiateRender(fiber);
|
||||||
return fiber.promise.then(() => this.component);
|
return fiber.promise.then(() => this.component);
|
||||||
|
|||||||
+15
-2
@@ -158,17 +158,30 @@ export class RootFiber extends Fiber {
|
|||||||
|
|
||||||
export let __internal__destroyed: ComponentNode[] = [];
|
export let __internal__destroyed: ComponentNode[] = [];
|
||||||
|
|
||||||
|
type Position = "first-child" | "last-child";
|
||||||
|
|
||||||
|
export interface MountOptions {
|
||||||
|
position?: Position;
|
||||||
|
}
|
||||||
|
|
||||||
export class MountFiber extends RootFiber {
|
export class MountFiber extends RootFiber {
|
||||||
target: HTMLElement;
|
target: HTMLElement;
|
||||||
|
position: Position;
|
||||||
|
|
||||||
constructor(node: ComponentNode, target: HTMLElement) {
|
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
|
||||||
super(node);
|
super(node);
|
||||||
this.target = target;
|
this.target = target;
|
||||||
|
this.position = options.position || "last-child";
|
||||||
}
|
}
|
||||||
complete() {
|
complete() {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
node.bdom = this.bdom;
|
node.bdom = this.bdom;
|
||||||
mount(node.bdom!, this.target);
|
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||||
|
mount(node.bdom!, this.target);
|
||||||
|
} else {
|
||||||
|
const firstChild = this.target.childNodes[0];
|
||||||
|
mount(node.bdom!, this.target, firstChild);
|
||||||
|
}
|
||||||
node.status = STATUS.MOUNTED;
|
node.status = STATUS.MOUNTED;
|
||||||
this.appliedToDom = true;
|
this.appliedToDom = true;
|
||||||
let current;
|
let current;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Component } from "./component";
|
|||||||
* Note that this method does modify in place the props
|
* Note that this method does modify in place the props
|
||||||
*/
|
*/
|
||||||
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
|
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
|
||||||
const defaultProps = (ComponentClass as any).defaultProps
|
const defaultProps = (ComponentClass as any).defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
for (let propName in defaultProps) {
|
for (let propName in defaultProps) {
|
||||||
if (props![propName] === undefined) {
|
if (props![propName] === undefined) {
|
||||||
@@ -16,7 +16,6 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Prop validation helper
|
// Prop validation helper
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1133,6 +1133,48 @@ exports[`basics zero or one child components 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`mount targets can mount a component (with default position='last-child') 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||||
|
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`mount targets can mount a component (with position='first-child') 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||||
|
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`mount targets default mount option is 'last-child' 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||||
|
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`support svg components add proper namespace to svg 1`] = `
|
exports[`support svg components add proper namespace to svg 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -902,78 +902,38 @@ describe("dynamic t-props", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip("mount targets", () => {
|
describe("mount targets", () => {
|
||||||
test("can attach a component to an existing node (if same tagname)", async () => {
|
|
||||||
// class App extends Component {
|
|
||||||
// static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
|
|
||||||
// state = useState({ customClass: "custom" });
|
|
||||||
// }
|
|
||||||
// const div = document.createElement("div");
|
|
||||||
// div.classList.add("arbitrary");
|
|
||||||
// div.innerHTML = `<p>pre-existing</p>`;
|
|
||||||
// fixture.appendChild(div);
|
|
||||||
// const app = await mount(App, { target: div, position: "self" });
|
|
||||||
// expect(fixture.innerHTML).toBe(
|
|
||||||
// `<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
|
|
||||||
// );
|
|
||||||
// expect(div).toBe(app.el);
|
|
||||||
// app.state.customClass = "custom2";
|
|
||||||
// await nextTick();
|
|
||||||
// expect(fixture.innerHTML).toBe(
|
|
||||||
// `<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
|
|
||||||
// );
|
|
||||||
// expect(div).toBe(app.el);
|
|
||||||
// app.unmount();
|
|
||||||
// // This assert is a best guess
|
|
||||||
// // The use case it covers was not really thought through
|
|
||||||
// // and may change in the future
|
|
||||||
// expect(fixture.innerHTML).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("cannot attach a component to an existing node (if not same tagname)", async () => {
|
|
||||||
// class App extends Component {
|
|
||||||
// static template = xml`<span>app</span>`;
|
|
||||||
// }
|
|
||||||
// const div = document.createElement("div");
|
|
||||||
// fixture.appendChild(div);
|
|
||||||
// let error;
|
|
||||||
// try {
|
|
||||||
// await mount(App, { target: div, position: "self" });
|
|
||||||
// } catch (e) {
|
|
||||||
// error = e;
|
|
||||||
// }
|
|
||||||
// expect(error).toBeDefined();
|
|
||||||
// expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can mount a component (with position='first-child')", async () => {
|
test("can mount a component (with position='first-child')", async () => {
|
||||||
// class App extends Component {
|
class Root extends Component {
|
||||||
// static template = xml`<div>app</div>`;
|
static template = xml`<div>app</div>`;
|
||||||
// }
|
}
|
||||||
// const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
// fixture.appendChild(span);
|
fixture.appendChild(span);
|
||||||
// await mount(App, { target: fixture, position: "first-child" });
|
const app = new App(Root);
|
||||||
// expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
|
await app.mount(fixture, { position: "first-child" });
|
||||||
|
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can mount a component (with position='last-child')", async () => {
|
test("can mount a component (with default position='last-child')", async () => {
|
||||||
// class App extends Component {
|
class Root extends Component {
|
||||||
// static template = xml`<div>app</div>`;
|
static template = xml`<div>app</div>`;
|
||||||
// }
|
}
|
||||||
// const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
// fixture.appendChild(span);
|
fixture.appendChild(span);
|
||||||
// await mount(App, { target: fixture, position: "last-child" });
|
const app = new App(Root);
|
||||||
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
await app.mount(fixture, { position: "last-child" });
|
||||||
|
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("default mount option is 'last-child'", async () => {
|
test("default mount option is 'last-child'", async () => {
|
||||||
// class App extends Component {
|
class Root extends Component {
|
||||||
// static template = xml`<div>app</div>`;
|
static template = xml`<div>app</div>`;
|
||||||
// }
|
}
|
||||||
// const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
// fixture.appendChild(span);
|
fixture.appendChild(span);
|
||||||
// await mount(App, { target: fixture });
|
const app = new App(Root);
|
||||||
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user