[IMP] app: improve API, small refactoring

This commit is contained in:
Géry Debongnie
2021-12-20 17:32:32 +01:00
committed by Mathieu Duckerts-Antoine
parent f4da50d350
commit 0b1c4dd4ef
13 changed files with 112 additions and 87 deletions
+17 -28
View File
@@ -2,7 +2,7 @@ import { Component } from "../component/component";
import { ComponentNode } from "../component/component_node"; import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers"; import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler"; import { Scheduler } from "../component/scheduler";
import { TemplateSet } from "./template_set"; import { TemplateSet, TemplateSetConfig } from "./template_set";
import { nodeErrorHandlers } from "../component/error_handling"; import { nodeErrorHandlers } from "../component/error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -11,12 +11,9 @@ export interface Env {
[key: string]: any; [key: string]: any;
} }
export interface AppConfig { export interface AppConfig extends TemplateSetConfig {
dev?: boolean;
env?: Env; env?: Env;
translatableAttributes?: string[]; props?: any;
translateFn?: (s: string) => string;
templates?: string | Document;
} }
export const DEV_MSG = `Owl is running in 'dev' mode. export const DEV_MSG = `Owl is running in 'dev' mode.
@@ -27,35 +24,19 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
export class App<T extends typeof Component = any> extends TemplateSet { export class App<T extends typeof Component = any> extends TemplateSet {
Root: T; Root: T;
props: any; props: any;
env: Env = Object.freeze({}); env: Env;
scheduler = new Scheduler(window.requestAnimationFrame.bind(window)); scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
root: ComponentNode | null = null; root: ComponentNode | null = null;
constructor(Root: T, props?: any) { constructor(Root: T, config: AppConfig = {}) {
super(); super(config);
this.Root = Root; this.Root = Root;
this.props = props;
}
configure(config: AppConfig): App<T> {
if (config.dev) { if (config.dev) {
this.dev = config.dev;
console.info(DEV_MSG); console.info(DEV_MSG);
} }
if (config.env) { const descrs = Object.getOwnPropertyDescriptors(config.env || {});
const descrs = Object.getOwnPropertyDescriptors(config.env); this.env = Object.freeze(Object.defineProperties({}, descrs));
this.env = Object.freeze(Object.defineProperties({}, descrs)); this.props = config.props || {};
}
if (config.translateFn) {
this.translateFn = config.translateFn;
}
if (config.translatableAttributes) {
this.translatableAttributes = config.translatableAttributes;
}
if (config.templates) {
this.addTemplates(config.templates);
}
return this;
} }
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> { mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
@@ -114,3 +95,11 @@ export class App<T extends typeof Component = any> extends TemplateSet {
} }
} }
} }
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement,
config: AppConfig & MountOptions = {}
): Promise<InstanceType<T>> {
return new App(C, config).mount(target, config);
}
+20 -8
View File
@@ -37,22 +37,34 @@ function parseXML(xml: string): Document {
return doc; return doc;
} }
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document;
}
export class TemplateSet { export class TemplateSet {
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates); rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string; translateFn?: (s: string) => string;
translatableAttributes?: string[]; translatableAttributes?: string[];
utils: typeof UTILS; utils: typeof UTILS = Object.assign({}, UTILS, {
dev?: boolean; call: (owner: any, subTemplate: string, ctx: any, parent: any) => {
constructor() {
const call = (owner: any, subTemplate: string, ctx: any, parent: any) => {
const template = this.getTemplate(subTemplate); const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent)); return toggler(subTemplate, template.call(owner, ctx, parent));
}; },
getTemplate: (name: string) => this.getTemplate(name),
});
const getTemplate = (name: string) => this.getTemplate(name); constructor(config: TemplateSetConfig = {}) {
this.utils = Object.assign({}, UTILS, { getTemplate, call }); this.dev = config.dev || false;
this.translateFn = config.translateFn;
this.translatableAttributes = config.translatableAttributes;
if (config.templates) {
this.addTemplates(config.templates);
}
} }
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) { addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
+10 -6
View File
@@ -13,6 +13,16 @@ import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation"; import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status"; import { STATUS } from "./status";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
export function useComponent(): Component {
return currentNode!.component;
}
export function component( export function component(
name: string | typeof Component, name: string | typeof Component,
props: any, props: any,
@@ -62,12 +72,6 @@ export function component(
// Component VNode // Component VNode
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
type LifecycleHook = Function; type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component> export class ComponentNode<T extends typeof Component = typeof Component>
+3 -22
View File
@@ -32,27 +32,9 @@ export const blockDom = {
comment, comment,
}; };
import type { AppConfig } from "./app/app"; export { App, mount } from "./app/app";
import { App } from "./app/app"; export { Component } from "./component/component";
import { Component } from "./component/component"; export { useComponent } from "./component/component_node";
import { getCurrent } from "./component/component_node";
export { App, Component };
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement,
config: AppConfig = {}
): Promise<InstanceType<T>> {
const app = new App(C);
return app.configure(config).mount(target);
}
export function useComponent(): Component {
const current = getCurrent();
return current!.component;
}
export { status } from "./component/status"; export { status } from "./component/status";
export { Portal } from "./portal"; export { Portal } from "./portal";
export { Memo } from "./memo"; export { Memo } from "./memo";
@@ -60,7 +42,6 @@ export { xml } from "./app/template_set";
export { useState, reactive } from "./reactivity"; export { useState, reactive } from "./reactivity";
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 {
onWillStart, onWillStart,
onMounted, onMounted,
@@ -15,6 +15,20 @@ exports[`app App supports env with getters/setters 1`] = `
}" }"
`; `;
exports[`app can configure an app with props 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['props'].value;
return block1([d1]);
}
}"
`;
exports[`app destroy remove the widget from the DOM 1`] = ` exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -40,8 +40,7 @@ describe("app", () => {
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="Object.keys(env.services)" /></div>`; static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="Object.keys(env.services)" /></div>`;
} }
const app = new App(SomeComponent); const app = new App(SomeComponent, { env });
app.configure({ env });
const comp = await app.mount(fixture); const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>maggot serv1</div>"); expect(fixture.innerHTML).toBe("<div>maggot serv1</div>");
someVal = "brain"; someVal = "brain";
@@ -50,4 +49,14 @@ describe("app", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>brain serv1,serv2</div>"); expect(fixture.innerHTML).toBe("<div>brain serv1,serv2</div>");
}); });
test("can configure an app with props", async () => {
class SomeComponent extends Component {
static template = xml`<div t-esc="props.value"/>`;
}
const app = new App(SomeComponent, { props: { value: 333 } });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>333</div>");
});
}); });
@@ -1060,6 +1060,19 @@ exports[`mount targets default mount option is 'last-child' 1`] = `
}" }"
`; `;
exports[`mount targets mount function: can mount a component (with default position='last-child') 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
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
) { ) {
+12 -2
View File
@@ -41,7 +41,7 @@ describe("basics", () => {
static template = xml`<span><t t-esc="props.value"/></span>`; static template = xml`<span><t t-esc="props.value"/></span>`;
} }
const app = new App(Test, { value: 3 }); const app = new App(Test, { props: { value: 3 } });
const component = await app.mount(fixture); const component = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span>3</span>"); expect(fixture.innerHTML).toBe("<span>3</span>");
@@ -125,7 +125,7 @@ describe("basics", () => {
} }
} }
const app = new App(Test, p); const app = new App(Test, { props: p });
await app.mount(fixture); await app.mount(fixture);
}); });
@@ -819,6 +819,16 @@ describe("mount targets", () => {
expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
test("mount function: can mount a component (with default position='last-child')", async () => {
class Root extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
await mount(Root, 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 Root extends Component { class Root extends Component {
static template = xml`<div>app</div>`; static template = xml`<div>app</div>`;
+1 -1
View File
@@ -623,7 +623,7 @@ test("rendering component again in next microtick", async () => {
} }
const env = { config: { flag: false } }; const env = { config: { flag: false } };
await new App(Parent).configure({ env }).mount(fixture); await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("<div><button>Click</button></div>"); expect(fixture.innerHTML).toBe("<div><button>Click</button></div>");
expect([ expect([
"Parent:setup", "Parent:setup",
+2 -2
View File
@@ -21,7 +21,7 @@ describe("env handling", () => {
class Test extends Component { class Test extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
} }
const component = await new App(Test).configure({ env }).mount(fixture); const component = await new App(Test, { env }).mount(fixture);
expect(Object.isFrozen(component.env)).toBeTruthy(); expect(Object.isFrozen(component.env)).toBeTruthy();
expect(component.env).toEqual({ foo: 42, bar: { value: 42 } }); expect(component.env).toEqual({ foo: 42, bar: { value: 42 } });
expect(() => { expect(() => {
@@ -47,7 +47,7 @@ describe("env handling", () => {
static components = { Child }; static components = { Child };
} }
await new App(Test).configure({ env }).mount(fixture); await new App(Test, { env }).mount(fixture);
expect(child.env).toEqual(env); expect(child.env).toEqual(env);
}); });
}); });
@@ -1,4 +1,4 @@
import { App, Component, mount, useState, xml } from "../../src"; import { Component, mount, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -64,7 +64,7 @@ describe("basics", () => {
static components = { Child, OtherChild }; static components = { Child, OtherChild };
} }
const env = { options: { flag: true } }; const env = { options: { flag: true } };
const parent = await new App(Parent).configure({ env }).mount(fixture); const parent = await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>"); expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
env.options.flag = false; env.options.flag = false;
+4 -4
View File
@@ -176,7 +176,7 @@ describe("hooks", () => {
} }
} }
const env = { val: 1 }; const env = { val: 1 };
await new App(Test).configure({ env }).mount(fixture); await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>1</div>"); expect(fixture.innerHTML).toBe("<div>1</div>");
}); });
@@ -188,7 +188,7 @@ describe("hooks", () => {
} }
} }
const env = { val: 3 }; const env = { val: 3 };
const component = await new App(Test).configure({ env }).mount(fixture); const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).not.toHaveProperty("val2"); expect(component.env).not.toHaveProperty("val2");
expect(component.env).toHaveProperty("val"); expect(component.env).toHaveProperty("val");
@@ -219,7 +219,7 @@ describe("hooks", () => {
return someVal; return someVal;
}, },
}; };
const component = await new App(Test).configure({ env }).mount(fixture); const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>maggot brain</div>"); expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
someVal = "brain"; someVal = "brain";
someVal2 = "maggot"; someVal2 = "maggot";
@@ -252,7 +252,7 @@ describe("hooks", () => {
} }
} }
const env = { val: 3 }; const env = { val: 3 };
await new App(Parent).configure({ env }).mount(fixture); await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("3<div>5</div>"); expect(fixture.innerHTML).toBe("3<div>5</div>");
}); });
+3 -10
View File
@@ -1,5 +1,4 @@
import { import {
App,
Component, Component,
mount, mount,
onError, onError,
@@ -432,10 +431,8 @@ describe("Portal", () => {
</div>`; </div>`;
} }
const env = {}; const env = {};
const app = new App(Parent);
app.configure({ env });
addOutsideDiv(fixture); addOutsideDiv(fixture);
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture, { env });
expect(parent.env).toStrictEqual({}); expect(parent.env).toStrictEqual({});
}); });
@@ -529,10 +526,8 @@ describe("Portal: Props validation", () => {
</div>`; </div>`;
} }
let error: Error; let error: Error;
let app = new App(Parent);
app.configure({ dev: true });
try { try {
await app.mount(fixture); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
@@ -555,10 +550,8 @@ describe("Portal: Props validation", () => {
</div>`; </div>`;
} }
let error: Error; let error: Error;
let app = new App(Parent);
app.configure({ dev: true });
try { try {
await app.mount(fixture); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }