add router service, better structure for demo app

This commit is contained in:
Géry Debongnie
2019-01-22 10:38:44 +01:00
parent 82490b2099
commit 4862496891
15 changed files with 242 additions and 91 deletions
+14 -6
View File
@@ -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;
}
}
-20
View File
@@ -1,20 +0,0 @@
import Widget from "../../src/core/widget";
const template = `
<div class="o_navbar">
<span class="title">Odoo</span>
<ul>
<li t-foreach="state.items" t-as="item">
<a href="someaction"><t t-esc="item.title"/></a>
</li>
</ul>
</div>
`;
export default class Navbar extends Widget {
name = "navbar";
template = template;
state = {
items: [{title: 'Discuss'}, {title: 'CRM'}]
};
}
-44
View File
@@ -1,44 +0,0 @@
import Widget from "../../src/core/widget";
import Counter from "./Counter";
import Navbar from "./Navbar";
const template = `
<div class="o_web_client">
<t t-widget="Navbar"/>
<div class="o_content">
<span>Root Widget</span>
<button t-on-click="resetCounter">Reset</button>
<button t-on-click="resetCounterAsync">Reset in 3s</button>
<button t-on-click="toggle">Toggle Counter</button>
<input/>
<t t-if="state.validcounter">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:4}"/>
</t>
<t t-else="1">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:7}"/>
</t>
<div ref="target"/>
</div>
</div>
`;
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});
}
}
+4 -1
View File
@@ -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;
+2 -3
View File
@@ -1,10 +1,9 @@
///<amd-module name="main" />
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);
});
+17
View File
@@ -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;
+64
View File
@@ -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<RouteInfo>) {
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 };
}
}
+12
View File
@@ -0,0 +1,12 @@
import Widget from "../../../src/core/widget";
const template = `
<div class="o_crm">
<span>CRM!!!!</span>
</div>
`;
export default class Discuss extends Widget {
name = "crm";
template = template;
}
@@ -1,4 +1,4 @@
import Widget from "../../src/core/widget";
import Widget from "../../../src/core/widget";
const template = `
<div>
+40
View File
@@ -0,0 +1,40 @@
import Widget from "../../../src/core/widget";
import Counter from "./Counter";
const template = `
<div class="o_discuss">
<span>Root Widget</span>
<button t-on-click="resetCounter">Reset</button>
<button t-on-click="resetCounterAsync">Reset in 3s</button>
<button t-on-click="toggle">Toggle Counter</button>
<input/>
<t t-if="state.validcounter">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:4}"/>
</t>
<t t-else="1">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:7}"/>
</t>
<div ref="target"/>
</div>
`;
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 });
}
}
+23
View File
@@ -0,0 +1,23 @@
import Widget from "../../../src/core/widget";
import { Action } from "../services/actions";
const template = `
<div class="o_navbar">
<span class="title">Odoo</span>
<ul>
<li t-foreach="env.services.actions" t-as="action">
<a t-att-href="getUrl(action)"><t t-esc="action.title"/></a>
</li>
</ul>
</div>
`;
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 });
}
}
+54
View File
@@ -0,0 +1,54 @@
import Widget, { Env } from "../../../src/core/widget";
import Navbar from "./Navbar";
import { Action } from "../services/actions";
const template = `
<div class="o_web_client">
<t t-widget="Navbar"/>
<div class="o_content">
<t t-widget="Content"/>
</div>
</div>
`;
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();
(<any>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;
}
}
+1 -1
View File
@@ -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"
},
+9 -13
View File
@@ -18,9 +18,9 @@ export default class Widget {
template: string = "<div></div>";
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
*
+1 -2
View File
@@ -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;
}