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 {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -6,26 +8,27 @@ html {
|
|||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Web Client */
|
/* Web Client */
|
||||||
.o_web_client {
|
.o_web_client {
|
||||||
|
font-family: sans-serif;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 40px auto;
|
grid-template-rows: $navbar-height auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.o_navbar {
|
.o_navbar {
|
||||||
background-color: #875A7B;
|
background-color: $main-color;
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
line-height: $navbar-height;
|
||||||
|
|
||||||
span.title {
|
span.title {
|
||||||
font-size: 22px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 8px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -36,13 +39,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding: 8px 12px;
|
margin: 0 4px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($main-color, 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
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 { Env } from "../../src/core/widget";
|
||||||
import QWeb from "../../src/core/qweb_vdom";
|
import QWeb from "../../src/core/qweb_vdom";
|
||||||
|
import Router from "./services/router";
|
||||||
|
import actions from "./services/actions";
|
||||||
|
|
||||||
const qweb = new QWeb();
|
const qweb = new QWeb();
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
const env: Env = {
|
const env: Env = {
|
||||||
qweb: qweb,
|
qweb: qweb,
|
||||||
services: {}
|
services: { router, actions }
|
||||||
};
|
};
|
||||||
|
|
||||||
export default env;
|
export default env;
|
||||||
|
|||||||
+2
-3
@@ -1,10 +1,9 @@
|
|||||||
///<amd-module name="main" />
|
///<amd-module name="main" />
|
||||||
|
|
||||||
import RootWidget from "./RootWidget";
|
import RootWidget from "./widgets/RootWidget";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async function() {
|
document.addEventListener("DOMContentLoaded", async function() {
|
||||||
const rootWidget = new RootWidget(null);
|
const rootWidget = new RootWidget(env);
|
||||||
rootWidget.setEnvironment(env);
|
|
||||||
await rootWidget.mount(document.body);
|
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 = `
|
const template = `
|
||||||
<div>
|
<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": "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: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: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",
|
"predemo:dev": "npm run demo:build",
|
||||||
"demo:dev": "npm-run-all --parallel \"demo:build:* -- --watch\" demo:serve"
|
"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>";
|
template: string = "<div></div>";
|
||||||
vnode: VNode | null = null;
|
vnode: VNode | null = null;
|
||||||
|
|
||||||
parent: Widget | null;
|
parent: Widget | null = null;
|
||||||
children: Widget[] = [];
|
children: Widget[] = [];
|
||||||
env: Env | null = null;
|
env: Env;
|
||||||
el: HTMLElement | null = null;
|
el: HTMLElement | null = null;
|
||||||
state: Object = {};
|
state: Object = {};
|
||||||
refs: { [key: string]: Widget } = {};
|
refs: { [key: string]: Widget } = {};
|
||||||
@@ -29,13 +29,13 @@ export default class Widget {
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
constructor(parent: Widget | null, props?: any) {
|
constructor(parent: Widget | Env, props?: any) {
|
||||||
this.parent = parent;
|
if (parent instanceof Widget) {
|
||||||
if (parent) {
|
this.parent = parent;
|
||||||
parent.children.push(this);
|
parent.children.push(this);
|
||||||
if (parent.env) {
|
this.env = Object.create(parent.env);
|
||||||
this.setEnvironment(parent.env);
|
} else {
|
||||||
}
|
this.env = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export default class Widget {
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.appendChild(this.el!);
|
target.appendChild(this.el!);
|
||||||
if (document.body.contains(target)) {
|
if (document.body.contains(target)) {
|
||||||
this.visitSubTree(w => w.mounted())
|
this.visitSubTree(w => w.mounted());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vnode;
|
return vnode;
|
||||||
@@ -71,10 +71,6 @@ export default class Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnvironment(env: Env) {
|
|
||||||
this.env = Object.create(env);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DOCSTRIGN
|
* DOCSTRIGN
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ function makeWidget(W: typeof Widget): Widget {
|
|||||||
qweb: new QWeb(),
|
qweb: new QWeb(),
|
||||||
services: {}
|
services: {}
|
||||||
};
|
};
|
||||||
const w = new W(null);
|
const w = new W(env);
|
||||||
w.setEnvironment(env);
|
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user