diff --git a/demo/app.scss b/demo/app.scss index 32161680..e4069162 100644 --- a/demo/app.scss +++ b/demo/app.scss @@ -1,3 +1,5 @@ +$navbar-height: 40px; +$main-color: #875A7B; html { height: 100%; @@ -6,26 +8,27 @@ html { body { height: 100%; margin: 0; - font-family: sans-serif; } /* Web Client */ .o_web_client { + font-family: sans-serif; height: 100%; display: grid; - grid-template-rows: 40px auto; + grid-template-rows: $navbar-height auto; } /* Navbar */ .o_navbar { - background-color: #875A7B; + background-color: $main-color; color: white; display: flex; + line-height: $navbar-height; span.title { - font-size: 22px; + font-size: 24px; font-weight: bold; - padding: 8px; + padding: 0 16px; } ul { @@ -36,13 +39,18 @@ body { } li { - padding: 8px 12px; + margin: 0 4px; font-size: 20px; + &:hover { + background-color: darken($main-color, 10); + } } a { color: white; text-decoration: none; + padding: 0 5px; + } } diff --git a/demo/src/Navbar.ts b/demo/src/Navbar.ts deleted file mode 100644 index f6fc2a7d..00000000 --- a/demo/src/Navbar.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Widget from "../../src/core/widget"; - -const template = ` -
- Odoo - -
-`; - -export default class Navbar extends Widget { - name = "navbar"; - template = template; - state = { - items: [{title: 'Discuss'}, {title: 'CRM'}] - }; -} diff --git a/demo/src/RootWidget.ts b/demo/src/RootWidget.ts deleted file mode 100644 index bc3113e8..00000000 --- a/demo/src/RootWidget.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Widget from "../../src/core/widget"; -import Counter from "./Counter"; -import Navbar from "./Navbar"; - -const template = ` -
- -
- Root Widget - - - - - - - - - - -
-
-
-`; - -export default class RootWidget extends Widget { - name = "root"; - template = template; - widgets = { Counter, Navbar }; - state = { validcounter: true}; - - resetCounter(ev: MouseEvent) { - this.refs.counter.updateState({counter: 3}) - } - - resetCounterAsync(ev: MouseEvent) { - setTimeout(() => { - this.refs.counter.updateState({counter: 3}) - }, 3000); - } - - toggle() { - this.updateState({validcounter: !this.state.validcounter}); - } -} diff --git a/demo/src/env.ts b/demo/src/env.ts index ebbf8e16..c579ec0f 100644 --- a/demo/src/env.ts +++ b/demo/src/env.ts @@ -1,11 +1,14 @@ import { Env } from "../../src/core/widget"; import QWeb from "../../src/core/qweb_vdom"; +import Router from "./services/router"; +import actions from "./services/actions"; const qweb = new QWeb(); +const router = new Router(); const env: Env = { qweb: qweb, - services: {} + services: { router, actions } }; export default env; diff --git a/demo/src/main.ts b/demo/src/main.ts index 46598977..46daa996 100644 --- a/demo/src/main.ts +++ b/demo/src/main.ts @@ -1,10 +1,9 @@ /// -import RootWidget from "./RootWidget"; +import RootWidget from "./widgets/RootWidget"; import env from "./env"; document.addEventListener("DOMContentLoaded", async function() { - const rootWidget = new RootWidget(null); - rootWidget.setEnvironment(env); + const rootWidget = new RootWidget(env); await rootWidget.mount(document.body); }); diff --git a/demo/src/services/actions.ts b/demo/src/services/actions.ts new file mode 100644 index 00000000..8ad5248a --- /dev/null +++ b/demo/src/services/actions.ts @@ -0,0 +1,17 @@ +import CRM from "../widgets/CRM"; +import Discuss from "../widgets/Discuss"; +import Widget from "../../../src/core/widget"; + +export interface Action { + id: number; + title: string; + Widget: typeof Widget; + default?: boolean; +} + +const actions: Action[] = [ + { id: 1, title: "Discuss", Widget: Discuss, default: true }, + { id: 2, title: "CRM", Widget: CRM } +]; + +export default actions; diff --git a/demo/src/services/router.ts b/demo/src/services/router.ts new file mode 100644 index 00000000..b8c4d71e --- /dev/null +++ b/demo/src/services/router.ts @@ -0,0 +1,64 @@ +export type Route = string; +export type Query = { [key: string]: string }; + +export interface RouteInfo { + route: Route; + query: Query; + title: string; +} + +export default class Router { + listeners: { owner: any; callback: (info: RouteInfo) => void }[] = []; + + constructor() { + window.addEventListener("popstate", this.onUrlChange.bind(this)); + } + + onUrlChange() { + const info = this.getRouteInfo(); + for (let listener of this.listeners) { + listener.callback.call(listener.owner, info); + } + } + + /** + * @param {Route} route relative route: for example, /web/ + * @param {Query} query + */ + navigate(info: Partial) { + const currentRouteInfo = this.getRouteInfo(); + const route = info.route || currentRouteInfo.route; + const query = info.query || {}; + const title = info.title || currentRouteInfo.title; + const url = this.formatURL(route, query); + window.history.pushState(null, title, url); + } + register(owner: any, callback: (info: RouteInfo) => void) { + this.listeners.push({ owner, callback }); + } + + unregister(owner: any) { + this.listeners = this.listeners.filter(l => l.owner !== owner); + } + + formatURL(route: Route, query: Query): string { + let url = route; + let hasHash = false; + for (let key in query) { + url = url + (hasHash ? "&" : "#"); + url = url + `${key}=${query[key]}`; + hasHash = true; + } + return url; + } + + getRouteInfo(): RouteInfo { + const route = window.location.pathname.slice(1); + const query = {}; + for (let part of window.location.hash.slice(1).split("?")) { + let [key, value] = part.split("="); + query[key] = value; + } + return { route, query, title: document.title }; + } +} diff --git a/demo/src/widgets/CRM.ts b/demo/src/widgets/CRM.ts new file mode 100644 index 00000000..5ccff573 --- /dev/null +++ b/demo/src/widgets/CRM.ts @@ -0,0 +1,12 @@ +import Widget from "../../../src/core/widget"; + +const template = ` +
+ CRM!!!! +
+`; + +export default class Discuss extends Widget { + name = "crm"; + template = template; +} diff --git a/demo/src/Counter.ts b/demo/src/widgets/Counter.ts similarity index 92% rename from demo/src/Counter.ts rename to demo/src/widgets/Counter.ts index 656c6da9..094b8e58 100644 --- a/demo/src/Counter.ts +++ b/demo/src/widgets/Counter.ts @@ -1,4 +1,4 @@ -import Widget from "../../src/core/widget"; +import Widget from "../../../src/core/widget"; const template = `
diff --git a/demo/src/widgets/Discuss.ts b/demo/src/widgets/Discuss.ts new file mode 100644 index 00000000..7c2e3c79 --- /dev/null +++ b/demo/src/widgets/Discuss.ts @@ -0,0 +1,40 @@ +import Widget from "../../../src/core/widget"; +import Counter from "./Counter"; + +const template = ` +
+ Root Widget + + + + + + + + + + +
+
+`; + +export default class Discuss extends Widget { + name = "discuss"; + template = template; + widgets = { Counter }; + state = { validcounter: true }; + + resetCounter(ev: MouseEvent) { + this.refs.counter.updateState({ counter: 3 }); + } + + resetCounterAsync(ev: MouseEvent) { + setTimeout(() => { + this.refs.counter.updateState({ counter: 3 }); + }, 3000); + } + + toggle() { + this.updateState({ validcounter: !this.state.validcounter }); + } +} diff --git a/demo/src/widgets/Navbar.ts b/demo/src/widgets/Navbar.ts new file mode 100644 index 00000000..099913ed --- /dev/null +++ b/demo/src/widgets/Navbar.ts @@ -0,0 +1,23 @@ +import Widget from "../../../src/core/widget"; +import { Action } from "../services/actions"; + +const template = ` +
+ Odoo +
    +
  • + +
  • +
+
+`; + +export default class Navbar extends Widget { + name = "navbar"; + template = template; + + getUrl(action: Action) { + const action_id = action.id; + return this.env.services.router.formatURL("web", { action_id }); + } +} diff --git a/demo/src/widgets/RootWidget.ts b/demo/src/widgets/RootWidget.ts new file mode 100644 index 00000000..82f579a3 --- /dev/null +++ b/demo/src/widgets/RootWidget.ts @@ -0,0 +1,54 @@ +import Widget, { Env } from "../../../src/core/widget"; +import Navbar from "./Navbar"; +import { Action } from "../services/actions"; + +const template = ` +
+ +
+ +
+
+`; + +export default class RootWidget extends Widget { + name = "root"; + template = template; + widgets = { Navbar }; + state = { validcounter: true }; + + constructor(env: Env) { + super(env); + this.setMainWidget(); + } + + mounted() { + this.env.services.router.register(this, this.onUrlChange); + } + + setMainWidget() { + const action = this.getAction(); + (this.widgets).Content = action.Widget; + } + + onUrlChange() { + this.setMainWidget(); + // notice that this can only be safely done because the root widget is + // mounted now. + this.render(); + } + + getAction(): Action { + const routeInfo = this.env.services.router.getRouteInfo(); + const actionID = parseInt(routeInfo.query.action_id); + let actions: Action[] = this.env.services.actions; + let action = actions.find(a => a.id === actionID); + if (!action) { + action = actions.find(a => a.default === true); + if (!action) { + throw new Error("No valid action!"); + } + } + return action; + } +} diff --git a/package.json b/package.json index 0345f963..cf8d6013 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "demo:build": "cp -r demo/ dist/demo && npm run demo:build:js && npm run demo:build:css", "demo:build:css": "sass demo/app.scss dist/demo/app.css", "demo:build:js": "tsc --allowjs -m amd --lib es2017,dom --target esnext --outfile dist/demo/main.js demo/src/main.ts", - "demo:serve": "live-server dist/demo/", + "demo:serve": "live-server --entry-file=index.html dist/demo/", "predemo:dev": "npm run demo:build", "demo:dev": "npm-run-all --parallel \"demo:build:* -- --watch\" demo:serve" }, diff --git a/src/core/widget.ts b/src/core/widget.ts index 858ffd57..3a8aac2b 100644 --- a/src/core/widget.ts +++ b/src/core/widget.ts @@ -18,9 +18,9 @@ export default class Widget { template: string = "
"; vnode: VNode | null = null; - parent: Widget | null; + parent: Widget | null = null; children: Widget[] = []; - env: Env | null = null; + env: Env; el: HTMLElement | null = null; state: Object = {}; refs: { [key: string]: Widget } = {}; @@ -29,13 +29,13 @@ export default class Widget { // Lifecycle //-------------------------------------------------------------------------- - constructor(parent: Widget | null, props?: any) { - this.parent = parent; - if (parent) { + constructor(parent: Widget | Env, props?: any) { + if (parent instanceof Widget) { + this.parent = parent; parent.children.push(this); - if (parent.env) { - this.setEnvironment(parent.env); - } + this.env = Object.create(parent.env); + } else { + this.env = parent; } } @@ -59,7 +59,7 @@ export default class Widget { if (target) { target.appendChild(this.el!); if (document.body.contains(target)) { - this.visitSubTree(w => w.mounted()) + this.visitSubTree(w => w.mounted()); } } return vnode; @@ -71,10 +71,6 @@ export default class Widget { } } - setEnvironment(env: Env) { - this.env = Object.create(env); - } - /** * DOCSTRIGN * diff --git a/tests/widget.test.ts b/tests/widget.test.ts index be9458e8..d5d6e3e8 100644 --- a/tests/widget.test.ts +++ b/tests/widget.test.ts @@ -6,8 +6,7 @@ function makeWidget(W: typeof Widget): Widget { qweb: new QWeb(), services: {} }; - const w = new W(null); - w.setEnvironment(env); + const w = new W(env); return w; }