mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
add router service, better structure for demo app
This commit is contained in:
+14
-6
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'}]
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user