diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/base_widget.ts similarity index 91% rename from web/static/src/ts/core/Widget.ts rename to web/static/src/ts/core/base_widget.ts index acc7f7b3..aa435110 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/base_widget.ts @@ -23,8 +23,8 @@ interface Meta { isStarted: boolean; isMounted: boolean; isDestroyed: boolean; - parent: Widget | null; - children: { [key: number]: Widget }; + parent: BaseWidget | null; + children: { [key: number]: BaseWidget }; // children mapping: from templateID to widgetID // should it be a map number => Widget? cmap: { [key: number]: number }; @@ -40,7 +40,7 @@ export interface Type extends Function { // Widget //------------------------------------------------------------------------------ -export class Widget extends EventBus { +export class BaseWidget extends EventBus { __widget__: Meta; template: string = "default"; inlineTemplate: string | null = null; @@ -52,13 +52,15 @@ export class Widget extends EventBus { env: T; state: Object = {}; props: Props; - refs: { [key: string]: Widget | HTMLElement | undefined } = {}; + refs: { + [key: string]: BaseWidget | HTMLElement | undefined; + } = {}; //-------------------------------------------------------------------------- // Lifecycle //-------------------------------------------------------------------------- - constructor(parent: Widget | T, props?: Props) { + constructor(parent: BaseWidget | T, props?: Props) { super(); wl.push(this); @@ -68,8 +70,8 @@ export class Widget extends EventBus { // Pro: but creating widget (by a template) is always unsafe anyway this.props = props; let id: number; - let p: Widget | null = null; - if (parent instanceof Widget) { + let p: BaseWidget | null = null; + if (parent instanceof BaseWidget) { p = parent; this.env = parent.env; id = this.env.getID(); @@ -173,17 +175,11 @@ export class Widget extends EventBus { * - it is ok to call updateState before the widget is started. In that * case, it will simply update the state and will not rerender */ - async updateState(nextState: Object) { + async updateState(nextState: Partial) { if (Object.keys(nextState).length === 0) { return; } - for (let key in nextState) { - if (key in this.state) { - this.state[key] = nextState[key]; - } else { - throw new Error(`Invalid key: '${key}' does not exist in widget state`); - } - } + Object.assign(this.state, nextState); if (this.__widget__.isStarted) { return this.render(); } @@ -259,7 +255,7 @@ export class Widget extends EventBus { } } - private visitSubTree(callback: (w: Widget) => boolean) { + private visitSubTree(callback: (w: BaseWidget) => boolean) { const shouldVisitChildren = callback(this); if (shouldVisitChildren) { const children = this.__widget__.children; diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts index 241101c2..3705efb8 100644 --- a/web/static/src/ts/env.ts +++ b/web/static/src/ts/env.ts @@ -1,18 +1,22 @@ -import { QWeb } from "./core/qweb_vdom"; -import { idGenerator, memoize } from "./core/utils"; -import { WEnv } from "./core/widget"; -import { ActionManager, IActionManager } from "./services/action_manager"; import { Ajax, IAjax } from "./core/ajax"; +import { WEnv } from "./core/base_widget"; import { INotificationManager, NotificationManager } from "./core/notifications"; -import { actionRegistry } from "./registries"; +import { QWeb } from "./core/qweb_vdom"; import { Registry } from "./core/registry"; import { IRouter, Router } from "./core/router"; +import { idGenerator, memoize } from "./core/utils"; +import { actionRegistry } from "./registries"; +import { + ActionManager, + ActionWidget, + IActionManager +} from "./services/action_manager"; import { CRM } from "./widgets/crm"; import { Discuss } from "./widgets/discuss"; -import { Widget, Type } from "./core/widget"; + //------------------------------------------------------------------------------ // Types //------------------------------------------------------------------------------ @@ -30,7 +34,7 @@ export interface Env extends WEnv { router: IRouter; // registries - actionRegistry: Registry>>; + actionRegistry: Registry; // data menus: Menu[]; diff --git a/web/static/src/ts/registries.ts b/web/static/src/ts/registries.ts index bf97d29e..7aa514fb 100644 --- a/web/static/src/ts/registries.ts +++ b/web/static/src/ts/registries.ts @@ -1,5 +1,4 @@ import { Registry } from "./core/registry"; -import { Widget, Type } from "./core/widget"; -import { Env } from "./env"; +import { ActionWidget } from "./services/action_manager"; -export const actionRegistry: Registry>> = new Registry(); +export const actionRegistry: Registry = new Registry(); diff --git a/web/static/src/ts/services/action_manager.ts b/web/static/src/ts/services/action_manager.ts index 5ec80570..65ccdb1a 100644 --- a/web/static/src/ts/services/action_manager.ts +++ b/web/static/src/ts/services/action_manager.ts @@ -1,7 +1,7 @@ +import { Type } from "../core/base_widget"; import { EventBus } from "../core/event_bus"; import { Registry } from "../core/registry"; -import { Type, Widget } from "../core/widget"; -import { Env } from "../env"; +import { Widget } from "../widgets/widget"; //------------------------------------------------------------------------------ // Types @@ -18,10 +18,12 @@ export interface CommonActionInfo { target: "current" | "new"; } +export type ActionWidget = Type>; + export interface ClientActionInfo extends CommonActionInfo { type: "client"; name: string; - Widget: Type>; + Widget: ActionWidget; } export interface ActWindowInfo extends CommonActionInfo { @@ -47,10 +49,10 @@ export interface IActionManager { //------------------------------------------------------------------------------ export class ActionManager extends EventBus implements IActionManager { - registry: Registry>>; + registry: Registry; stack: ActionStack; - constructor(registry: Registry>>) { + constructor(registry: Registry) { super(); this.registry = registry; this.stack = []; diff --git a/web/static/src/ts/widgets/CRM.ts b/web/static/src/ts/widgets/CRM.ts index b4f88564..b8912398 100644 --- a/web/static/src/ts/widgets/CRM.ts +++ b/web/static/src/ts/widgets/CRM.ts @@ -1,6 +1,5 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; +import { Widget } from "./widget"; -export class CRM extends Widget { +export class CRM extends Widget<{}, {}> { template = "web.crm"; } diff --git a/web/static/src/ts/widgets/Counter.ts b/web/static/src/ts/widgets/Counter.ts index f42a8b94..43fce3e7 100644 --- a/web/static/src/ts/widgets/Counter.ts +++ b/web/static/src/ts/widgets/Counter.ts @@ -1,22 +1,34 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; +import { Widget } from "./widget"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ interface Props { initialState?: number; } -export class Counter extends Widget { +interface State { + counter: number; +} + +//------------------------------------------------------------------------------ +// Counter +//------------------------------------------------------------------------------ + +export class Counter extends Widget { inlineTemplate = `
Value:
`; + state = { counter: 0 }; - constructor(parent: Widget, props: Props) { + constructor(parent: Widget, props: Props) { super(parent, props); this.state.counter = props.initialState || 0; } diff --git a/web/static/src/ts/widgets/Discuss.ts b/web/static/src/ts/widgets/Discuss.ts index 896b9985..5cced964 100644 --- a/web/static/src/ts/widgets/Discuss.ts +++ b/web/static/src/ts/widgets/Discuss.ts @@ -1,9 +1,21 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; import { Clock } from "./clock"; import { Counter } from "./counter"; +import { Widget } from "./widget"; -export class Discuss extends Widget { +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +interface State { + validcounter: boolean; + color: "red" | "blue"; +} + +//------------------------------------------------------------------------------ +// Discuss +//------------------------------------------------------------------------------ + +export class Discuss extends Widget<{}, State> { template = "web.discuss"; widgets = { Clock, Counter, ColorWidget }; state = { validcounter: true, color: "red" }; @@ -38,7 +50,7 @@ export class Discuss extends Widget { } } -class ColorWidget extends Widget { +class ColorWidget extends Widget<{ color: "red" | "blue" }, {}> { inlineTemplate = `
Current Color: diff --git a/web/static/src/ts/widgets/Navbar.ts b/web/static/src/ts/widgets/Navbar.ts index f8fb8c21..4145c0b2 100644 --- a/web/static/src/ts/widgets/Navbar.ts +++ b/web/static/src/ts/widgets/Navbar.ts @@ -1,13 +1,21 @@ -import { Widget } from "../core/widget"; -import { Env, Menu } from "../env"; +import { Menu } from "../env"; import { MenuItem } from "../misc/menu_helpers"; +import { Widget } from "./widget"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ export interface Props { inHome: boolean; app: MenuItem | null; } -export class Navbar extends Widget { +//------------------------------------------------------------------------------ +// Navbar +//------------------------------------------------------------------------------ + +export class Navbar extends Widget { template = "web.navbar"; getUrl(menu: Menu) { diff --git a/web/static/src/ts/widgets/action_container.ts b/web/static/src/ts/widgets/action_container.ts index 08b4cb2d..a898e9de 100644 --- a/web/static/src/ts/widgets/action_container.ts +++ b/web/static/src/ts/widgets/action_container.ts @@ -1,12 +1,19 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; import { ActionStack } from "../services/action_manager"; +import { Widget } from "./widget"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ export interface Props { stack: ActionStack; } -export class ActionContainer extends Widget { +//------------------------------------------------------------------------------ +// Action Container +//------------------------------------------------------------------------------ + +export class ActionContainer extends Widget { template = "web.action_container"; currentWidget: any; diff --git a/web/static/src/ts/widgets/clock.ts b/web/static/src/ts/widgets/clock.ts index 01552374..f08b2305 100644 --- a/web/static/src/ts/widgets/clock.ts +++ b/web/static/src/ts/widgets/clock.ts @@ -1,7 +1,18 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; +import { Widget } from "./widget"; -export class Clock extends Widget { +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +interface State { + currentTime: string; +} + +//------------------------------------------------------------------------------ +// Clock +//------------------------------------------------------------------------------ + +export class Clock extends Widget<{}, State> { inlineTemplate = `
`; timeout: any | undefined; diff --git a/web/static/src/ts/widgets/home_menu.ts b/web/static/src/ts/widgets/home_menu.ts index aba94737..2fc02511 100644 --- a/web/static/src/ts/widgets/home_menu.ts +++ b/web/static/src/ts/widgets/home_menu.ts @@ -1,6 +1,5 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; import { MenuInfo, MenuItem } from "../misc/menu_helpers"; +import { Widget } from "./widget"; //------------------------------------------------------------------------------ // Types @@ -14,7 +13,7 @@ export interface Props { // Home Menu //------------------------------------------------------------------------------ -export class HomeMenu extends Widget { +export class HomeMenu extends Widget { template = "web.home_menu"; get apps(): MenuItem[] { diff --git a/web/static/src/ts/widgets/notification.ts b/web/static/src/ts/widgets/notification.ts index a6432e83..c4a7a7c5 100644 --- a/web/static/src/ts/widgets/notification.ts +++ b/web/static/src/ts/widgets/notification.ts @@ -1,8 +1,7 @@ -import { Widget } from "../core/widget"; -import { Env } from "../env"; import { INotification } from "../core/notifications"; +import { Widget } from "./widget"; -export class Notification extends Widget { +export class Notification extends Widget { template = "web.notification"; close() { diff --git a/web/static/src/ts/widgets/root.ts b/web/static/src/ts/widgets/root.ts index 02cc30ea..b3adbf53 100644 --- a/web/static/src/ts/widgets/root.ts +++ b/web/static/src/ts/widgets/root.ts @@ -1,5 +1,5 @@ import { INotification } from "../core/notifications"; -import { Widget } from "../core/widget"; +import { Widget } from "./widget"; import { Env } from "../env"; import { MenuInfo, MenuItem, getAppAndAction } from "../misc/menu_helpers"; import { ActionStack } from "../services/action_manager"; @@ -28,7 +28,7 @@ interface Props { // Root Widget //------------------------------------------------------------------------------ -export class Root extends Widget { +export class Root extends Widget { template = "web.web_client"; widgets = { Navbar, Notification, HomeMenu, ActionContainer }; diff --git a/web/static/src/ts/widgets/widget.ts b/web/static/src/ts/widgets/widget.ts new file mode 100644 index 00000000..6ec33baf --- /dev/null +++ b/web/static/src/ts/widgets/widget.ts @@ -0,0 +1,4 @@ +import { BaseWidget } from "../core/base_widget"; +import { Env } from "../env"; + +export class Widget extends BaseWidget {} diff --git a/web/static/tests/core/widget.test.ts b/web/static/tests/core/base_widget.test.ts similarity index 90% rename from web/static/tests/core/widget.test.ts rename to web/static/tests/core/base_widget.test.ts index 4372705d..2b073856 100644 --- a/web/static/tests/core/widget.test.ts +++ b/web/static/tests/core/base_widget.test.ts @@ -1,6 +1,5 @@ -import { WEnv, Widget } from "../../src/ts/core/widget"; -import { makeTestWEnv, makeTestFixture } from "../helpers"; -import { normalize } from "../helpers"; +import { BaseWidget, WEnv } from "../../src/ts/core/base_widget"; +import { makeTestFixture, makeTestWEnv, normalize } from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers @@ -34,13 +33,16 @@ function nextTick(): Promise { return Promise.resolve(); } -function children(w: Widget): Widget[] { +class Widget extends BaseWidget {} + +function children(w: Widget): Widget[] { const childrenMap = w.__widget__.children; return Object.keys(childrenMap).map(id => childrenMap[id]); } // Test widgets -class Counter extends Widget { +class Counter extends Widget { + // class Counter extends Widget { template = "counter"; state = { counter: 0 @@ -51,12 +53,12 @@ class Counter extends Widget { } } -class WidgetA extends Widget { +class WidgetA extends Widget { template = "widgetA"; widgets = { b: WidgetB }; } -class WidgetB extends Widget { +class WidgetB extends Widget { template = "widgetB"; } @@ -88,7 +90,7 @@ describe("basic widget properties", () => { }); test("widget style and classname", async () => { - class StyledWidget extends Widget { + class StyledWidget extends Widget { inlineTemplate = `
world
`; } const widget = new StyledWidget(env); @@ -100,7 +102,7 @@ describe("basic widget properties", () => { test("updateState before first render does not trigger a render", async () => { let renderCalls = 0; - class TestW extends Widget { + class TestW extends Widget { async willStart() { this.updateState({}); } @@ -142,7 +144,7 @@ describe("basic widget properties", () => { describe("lifecycle hooks", () => { test("willStart hook is called", async () => { let willstart = false; - class HookWidget extends Widget { + class HookWidget extends Widget { async willStart() { willstart = true; } @@ -154,7 +156,7 @@ describe("lifecycle hooks", () => { test("mounted hook is not called if not in DOM", async () => { let mounted = false; - class HookWidget extends Widget { + class HookWidget extends Widget { async mounted() { mounted = true; } @@ -167,7 +169,7 @@ describe("lifecycle hooks", () => { test("mounted hook is called if mounted in DOM", async () => { let mounted = false; - class HookWidget extends Widget { + class HookWidget extends Widget { async mounted() { mounted = true; } @@ -179,11 +181,11 @@ describe("lifecycle hooks", () => { test("willStart hook is called on subwidget", async () => { let ok = false; - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; } - class ChildWidget extends Widget { + class ChildWidget extends Widget { async willStart() { ok = true; } @@ -197,7 +199,7 @@ describe("lifecycle hooks", () => { expect.assertions(4); let parentMounted = false; let childMounted = false; - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; mounted() { @@ -205,7 +207,7 @@ describe("lifecycle hooks", () => { parentMounted = true; } } - class ChildWidget extends Widget { + class ChildWidget extends Widget { mounted() { expect(document.body.contains(this.el)).toBe(true); expect(parentMounted).toBe(true); @@ -223,7 +225,7 @@ describe("lifecycle hooks", () => { // the t-else part in the template is important. This is // necessary to have a situation that could confuse the vdom // patching algorithm - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
@@ -236,7 +238,7 @@ describe("lifecycle hooks", () => { state = { ok: false }; widgets = { child: ChildWidget }; } - class ChildWidget extends Widget { + class ChildWidget extends Widget { async willStart() { hookCounter++; } @@ -261,13 +263,13 @@ describe("lifecycle hooks", () => { expect.assertions(1); const target = document.createElement("div"); document.body.appendChild(target); - class ParentWidget extends Widget { + class ParentWidget extends Widget { mounted() { const child = new ChildWidget(this); child.mount(this.el!); } } - class ChildWidget extends Widget { + class ChildWidget extends Widget { mounted() { expect(this.el).toBeTruthy(); done(); @@ -279,7 +281,7 @@ describe("lifecycle hooks", () => { test("widgets are unmounted and destroyed if no longer in DOM", async () => { let steps: string[] = []; - class ParentWidget extends Widget { + class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
@@ -287,7 +289,7 @@ describe("lifecycle hooks", () => { widgets = { child: ChildWidget }; } - class ChildWidget extends Widget { + class ChildWidget extends Widget { constructor(parent) { super(parent); steps.push("init"); @@ -320,7 +322,7 @@ describe("lifecycle hooks", () => { test("hooks are called in proper order in widget creation/destruction", async () => { let steps: string[] = []; - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; constructor(parent) { @@ -341,7 +343,7 @@ describe("lifecycle hooks", () => { } } - class ChildWidget extends Widget { + class ChildWidget extends Widget { constructor(parent) { super(parent); steps.push("c init"); @@ -378,7 +380,7 @@ describe("lifecycle hooks", () => { test("shouldUpdate hook prevent rerendering", async () => { let shouldUpdate = false; - class TestWidget extends Widget { + class TestWidget extends Widget { inlineTemplate = `
`; shouldUpdate() { return shouldUpdate; @@ -408,7 +410,7 @@ describe("destroy method", () => { test("destroying a widget twice only call destroyed once", async () => { let count = 0; - class TestWidget extends Widget { + class TestWidget extends Widget { destroyed() { count++; } @@ -450,7 +452,7 @@ describe("destroy method", () => { let p: Promise = new Promise(function(r) { resolve = r; }); - class DelayedWidget extends Widget { + class DelayedWidget extends Widget { willStart() { return p; } @@ -490,7 +492,7 @@ describe("composition", () => { }); test("t-refs on widget are widgets", async () => { - class WidgetC extends Widget { + class WidgetC extends Widget { inlineTemplate = `
Hello
`; widgets = { b: WidgetB }; } @@ -500,7 +502,7 @@ describe("composition", () => { }); test("modifying a sub widget", async () => { - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { Counter }; } @@ -538,7 +540,7 @@ describe("composition", () => { }); test("rerendering a widget with a sub widget", async () => { - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { Counter }; } @@ -557,7 +559,7 @@ describe("composition", () => { }); test("sub widgets are destroyed if no longer in dom, then recreated", async () => { - class ParentWidget extends Widget { + class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; widgets = { counter: Counter }; @@ -579,7 +581,7 @@ describe("composition", () => { }); test("sub widgets with t-keep-alive are not destroyed if no longer in dom", async () => { - class ParentWidget extends Widget { + class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; widgets = { counter: Counter }; @@ -605,12 +607,12 @@ describe("composition", () => { }); test("sub widgets dom state with t-keep-alive is preserved", async () => { - class ParentWidget extends Widget { + class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; widgets = { InputWidget }; } - class InputWidget extends Widget { + class InputWidget extends Widget { inlineTemplate = ""; } const widget = new ParentWidget(env); @@ -627,11 +629,11 @@ describe("composition", () => { }); test("sub widgets rendered in a loop", async () => { - class ChildWidget extends Widget { + class ChildWidget extends Widget { inlineTemplate = ``; } - class Parent extends Widget { + class Parent extends Widget { inlineTemplate = `
@@ -658,7 +660,7 @@ describe("composition", () => { test("sub widgets with some state rendered in a loop", async () => { let n = 1; - class ChildWidget extends Widget { + class ChildWidget extends Widget { inlineTemplate = ``; constructor(parent) { super(parent); @@ -667,7 +669,7 @@ describe("composition", () => { } } - class Parent extends Widget { + class Parent extends Widget { inlineTemplate = `
@@ -694,13 +696,13 @@ describe("composition", () => { describe("props evaluation (with t-props directive)", () => { test("explicit object prop", async () => { - class Parent extends Widget { + class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { val: 42 }; } - class Child extends Widget { + class Child extends Widget { inlineTemplate = ``; state: { someval: number }; constructor(parent: Parent, props: { value: number }) { @@ -715,13 +717,13 @@ describe("props evaluation (with t-props directive)", () => { }); test("object prop value", async () => { - class Parent extends Widget { + class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { val: 42 }; } - class Child extends Widget { + class Child extends Widget { inlineTemplate = ``; state: { someval: number }; constructor(parent: Parent, props: { val: number }) { @@ -739,7 +741,7 @@ describe("props evaluation (with t-props directive)", () => { describe("t-on directive on widgets", () => { test("t-on works as expected", async () => { let n = 0; - class ParentWidget extends Widget { + class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: Child }; someMethod(arg) { @@ -747,7 +749,7 @@ describe("t-on directive on widgets", () => { n++; } } - class Child extends Widget {} + class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; @@ -765,7 +767,7 @@ describe("random stuff/miscellaneous", () => { // this test makes sure that the foreach directive does not pollute sub // context with the inLoop variable, which is then used in the t-widget // directive as a key - class Test extends Widget { + class Test extends Widget { inlineTemplate = `
txt
`; widgets = { widget: Widget }; } @@ -778,13 +780,13 @@ describe("random stuff/miscellaneous", () => { // in this situation, we protect against a bug that occurred: because of the // interplay between widgets and vnodes, a sub widget vnode was patched // twice. - class Parent extends Widget { + class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { flag: false }; } - class Child extends Widget { + class Child extends Widget { inlineTemplate = `abcdef`; } diff --git a/web/static/tests/helpers.ts b/web/static/tests/helpers.ts index 0d6fb86e..0d67a1d9 100644 --- a/web/static/tests/helpers.ts +++ b/web/static/tests/helpers.ts @@ -1,6 +1,6 @@ import { QWeb } from "../src/ts/core/qweb_vdom"; import { idGenerator } from "../src/ts/core/utils"; -import { WEnv } from "../src/ts/core/widget"; +import { WEnv } from "../src/ts/core/base_widget"; import { Env } from "../src/ts/env"; import { IAjax, RPCQuery } from "../src/ts/core/ajax"; import { Registry } from "../src/ts/core/registry";