mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
refactoring, work on environment
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
roots: ["<rootDir>/web/static", "<rootDir>/tests"],
|
roots: ["<rootDir>/web/static"],
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.ts?$": "ts-jest"
|
"^.+\\.ts?$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,7 @@
|
|||||||
"build": "cp -r web/ dist/app && npm run build:js && npm run build:css",
|
"build": "cp -r web/ dist/app && npm run build:js && npm run build:css",
|
||||||
"build:css": "sass web/static/src/scss/app.scss dist/app/app.css",
|
"build:css": "sass web/static/src/scss/app.scss dist/app/app.css",
|
||||||
"build:js": "tsc --allowjs -m amd --lib es2017,dom --target esnext --outfile dist/app/main.js web/static/src/ts/main.ts",
|
"build:js": "tsc --allowjs -m amd --lib es2017,dom --target esnext --outfile dist/app/main.js web/static/src/ts/main.ts",
|
||||||
|
"minify": "uglifyjs dist/app/main.js -o dist/app/main.js --compress --mangle",
|
||||||
"serve": "live-server --entry-file=index.html dist/app/",
|
"serve": "live-server --entry-file=index.html dist/app/",
|
||||||
"predev": "npm run build",
|
"predev": "npm run build",
|
||||||
"dev": "npm-run-all --parallel \"build:* -- --watch\" serve"
|
"dev": "npm-run-all --parallel \"build:* -- --watch\" serve"
|
||||||
@@ -34,7 +35,8 @@
|
|||||||
"snabbdom-to-html": "^5.1.1",
|
"snabbdom-to-html": "^5.1.1",
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^23.10.5",
|
||||||
"typescript": "^3.2.2"
|
"typescript": "^3.2.2",
|
||||||
|
"uglify-es": "^3.3.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"snabbdom": "^0.7.3"
|
"snabbdom": "^0.7.3"
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { init } from "../../../libs/snabbdom/src/snabbdom";
|
|
||||||
import sdListeners from "../../../libs/snabbdom/src/modules/eventlisteners";
|
|
||||||
import sdAttrs from "../../../libs/snabbdom/src/modules/attributes";
|
import sdAttrs from "../../../libs/snabbdom/src/modules/attributes";
|
||||||
|
import sdListeners from "../../../libs/snabbdom/src/modules/eventlisteners";
|
||||||
|
import { init } from "../../../libs/snabbdom/src/snabbdom";
|
||||||
import { VNode } from "../../../libs/snabbdom/src/vnode";
|
import { VNode } from "../../../libs/snabbdom/src/vnode";
|
||||||
import QWeb from "./qweb_vdom";
|
import QWeb from "./qweb_vdom";
|
||||||
|
|
||||||
const patch = init([sdListeners, sdAttrs]);
|
const patch = init([sdListeners, sdAttrs]);
|
||||||
|
|
||||||
export interface WidgetEnv {
|
export interface WEnv {
|
||||||
qweb: QWeb;
|
qweb: QWeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Widget<T extends WidgetEnv> {
|
export default class Widget<T extends WEnv> {
|
||||||
name: string = "widget";
|
name: string = "widget";
|
||||||
template: string = "<div></div>";
|
template: string = "<div></div>";
|
||||||
vnode: VNode | null = null;
|
vnode: VNode | null = null;
|
||||||
@@ -49,7 +49,7 @@ export default class Widget<T extends WidgetEnv> {
|
|||||||
|
|
||||||
async mount(target?: HTMLElement): Promise<VNode> {
|
async mount(target?: HTMLElement): Promise<VNode> {
|
||||||
await this.willStart();
|
await this.willStart();
|
||||||
this.env!.qweb.addTemplate(this.name, this.template);
|
this.env.qweb.addTemplate(this.name, this.template);
|
||||||
delete this.template;
|
delete this.template;
|
||||||
const vnode = await this.render();
|
const vnode = await this.render();
|
||||||
|
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ const forEachDirective: Directive = {
|
|||||||
const onDirective: Directive = {
|
const onDirective: Directive = {
|
||||||
name: "on",
|
name: "on",
|
||||||
priority: 90,
|
priority: 90,
|
||||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
atNodeCreation({ ctx, fullName, value, nodeID, qweb }) {
|
||||||
const eventName = fullName.slice(5);
|
const eventName = fullName.slice(5);
|
||||||
let extraArgs;
|
let extraArgs;
|
||||||
let handler = value.replace(/\(.*\)/, function(args) {
|
let handler = value.replace(/\(.*\)/, function(args) {
|
||||||
@@ -649,7 +649,7 @@ const onDirective: Directive = {
|
|||||||
});
|
});
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`p${nodeID}.on = {${eventName}: context['${handler}'].bind(context${
|
`p${nodeID}.on = {${eventName}: context['${handler}'].bind(context${
|
||||||
extraArgs ? ", " + extraArgs : ""
|
extraArgs ? ", " + qweb._formatExpression(extraArgs) : ""
|
||||||
})}`
|
})}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import QWeb from "./core/qweb_vdom";
|
import QWeb from "./core/qweb_vdom";
|
||||||
|
import { WEnv } from "./core/widget";
|
||||||
|
import actions, { Action } from "./services/actions";
|
||||||
|
import ActionManager from "./services/action_manager";
|
||||||
|
import Ajax from "./services/ajax";
|
||||||
import Router from "./services/router";
|
import Router from "./services/router";
|
||||||
import actions from "./services/actions";
|
|
||||||
import { Env } from "./types";
|
|
||||||
|
|
||||||
const env: Env = {
|
export interface Env extends WEnv {
|
||||||
qweb: new QWeb(),
|
actionManager: ActionManager;
|
||||||
router: new Router(),
|
actions: Action[];
|
||||||
services: { actions }
|
ajax: Ajax;
|
||||||
};
|
router: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEnvironment(): Env {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
const router = new Router();
|
||||||
|
const ajax = new Ajax();
|
||||||
|
const actionManager = new ActionManager();
|
||||||
|
|
||||||
|
return {
|
||||||
|
qweb,
|
||||||
|
ajax,
|
||||||
|
router,
|
||||||
|
actionManager,
|
||||||
|
actions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default env;
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
///<amd-module name="main" />
|
///<amd-module name="main" />
|
||||||
|
|
||||||
import RootWidget from "./widgets/root_widget";
|
import RootWidget from "./widgets/root_widget";
|
||||||
import env from "./env";
|
import {makeEnvironment} from "./env";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async function() {
|
document.addEventListener("DOMContentLoaded", async function() {
|
||||||
|
const env = makeEnvironment();
|
||||||
const rootWidget = new RootWidget(env);
|
const rootWidget = new RootWidget(env);
|
||||||
await rootWidget.mount(document.body);
|
await rootWidget.mount(document.body);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Action } from "./actions";
|
||||||
|
|
||||||
|
// export interface Action {
|
||||||
|
// id: number;
|
||||||
|
// title: string;
|
||||||
|
// Widget: Type<Widget<Env>>;
|
||||||
|
// default?: boolean;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const actions: Action[] = [
|
||||||
|
// { id: 1, title: "Discuss", Widget: Discuss, default: true },
|
||||||
|
// { id: 2, title: "CRM", Widget: CRM }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class ActionManager {
|
||||||
|
doAction(action: Action) {
|
||||||
|
console.log(action);
|
||||||
|
// load data (??)
|
||||||
|
// trigger action somewhere
|
||||||
|
// upload url with router
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import CRM from "../widgets/crm";
|
import CRM from "../widgets/crm";
|
||||||
import Discuss from "../widgets/discuss";
|
import Discuss from "../widgets/discuss";
|
||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import { Env, Type } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
|
|
||||||
|
interface Type<T> extends Function {
|
||||||
|
new (...args: any[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -19,3 +21,9 @@ const actions: Action[] = [
|
|||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
|
|
||||||
|
// class ActionManager {
|
||||||
|
|
||||||
|
// doAction(action: Action) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
export default class Ajax{}
|
||||||
@@ -1,39 +1,46 @@
|
|||||||
export type Route = string;
|
|
||||||
export type Query = { [key: string]: string };
|
export type Query = { [key: string]: string };
|
||||||
|
|
||||||
export interface RouteInfo {
|
export interface Route {
|
||||||
route: Route;
|
// this is the part before the hash: www.something.com/web#action=1 => web
|
||||||
|
path: string;
|
||||||
query: Query;
|
query: Query;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearSlaches(s: string): string {
|
||||||
|
return s.replace(/\/$/, "").replace(/^\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
export default class Router {
|
export default class Router {
|
||||||
listeners: { owner: any; callback: (info: RouteInfo) => void }[] = [];
|
listeners: { owner: any; callback: (info: Route) => void }[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
window.addEventListener("popstate", this.onUrlChange.bind(this));
|
window.addEventListener("popstate", this.onUrlChange.bind(this));
|
||||||
|
window.onhashchange = function () { console.log('aaaa'); debugger; }
|
||||||
}
|
}
|
||||||
|
|
||||||
onUrlChange() {
|
onUrlChange(event: PopStateEvent) {
|
||||||
const info = this.getRouteInfo();
|
debugger
|
||||||
for (let listener of this.listeners) {
|
event.preventDefault();
|
||||||
listener.callback.call(listener.owner, info);
|
const info = this.getRoute();
|
||||||
}
|
for (let listener of this.listeners) {
|
||||||
|
listener.callback.call(listener.owner, info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Route} route relative route: for example, /web/
|
* @param {Route} route relative route: for example, /web/
|
||||||
* @param {Query} query
|
* @param {Query} query
|
||||||
*/
|
*/
|
||||||
navigate(info: Partial<RouteInfo>) {
|
navigate(info: Partial<Route>) {
|
||||||
const currentRouteInfo = this.getRouteInfo();
|
const currentRoute = this.getRoute();
|
||||||
const route = info.route || currentRouteInfo.route;
|
const route = info.path || currentRoute.path;
|
||||||
const query = info.query || {};
|
const query = info.query || {};
|
||||||
const title = info.title || currentRouteInfo.title;
|
const title = info.title || currentRoute.title;
|
||||||
const url = this.formatURL(route, query);
|
const url = this.formatURL(route, query);
|
||||||
window.history.pushState(null, title, url);
|
window.history.pushState(null, title, url);
|
||||||
}
|
}
|
||||||
register(owner: any, callback: (info: RouteInfo) => void) {
|
register(owner: any, callback: (info: Route) => void) {
|
||||||
this.listeners.push({ owner, callback });
|
this.listeners.push({ owner, callback });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,24 +48,24 @@ export default class Router {
|
|||||||
this.listeners = this.listeners.filter(l => l.owner !== owner);
|
this.listeners = this.listeners.filter(l => l.owner !== owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatURL(route: Route, query: Query): string {
|
formatURL(path: string, query: Query): string {
|
||||||
let url = route;
|
let url = clearSlaches(path);
|
||||||
let hasHash = false;
|
let hasHash = false;
|
||||||
for (let key in query) {
|
for (let key in query) {
|
||||||
url = url + (hasHash ? "&" : "#");
|
url = url + (hasHash ? "&" : "#");
|
||||||
url = url + `${key}=${query[key]}`;
|
url = url + `${key}=${query[key]}`;
|
||||||
hasHash = true;
|
hasHash = true;
|
||||||
}
|
}
|
||||||
return url;
|
return "/" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRouteInfo(): RouteInfo {
|
getRoute(): Route {
|
||||||
const route = window.location.pathname.slice(1);
|
const path = clearSlaches(window.location.pathname);
|
||||||
const query = {};
|
const query = {};
|
||||||
for (let part of window.location.hash.slice(1).split("?")) {
|
for (let part of window.location.hash.slice(1).split("?")) {
|
||||||
let [key, value] = part.split("=");
|
let [key, value] = part.split("=");
|
||||||
query[key] = value;
|
query[key] = value;
|
||||||
}
|
}
|
||||||
return { route, query, title: document.title };
|
return { path, query, title: document.title };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { WidgetEnv } from "./core/widget";
|
|
||||||
import Router from "./services/router";
|
|
||||||
|
|
||||||
export interface Env extends WidgetEnv {
|
|
||||||
router: Router;
|
|
||||||
services: { [key: string]: any };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Type<T> extends Function {
|
|
||||||
new (...args: any[]): T;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import { Env } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="o_crm">
|
<div class="o_crm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import { Env } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import Counter from "./counter";
|
import Counter from "./counter";
|
||||||
import { Env } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="o_discuss">
|
<div class="o_discuss">
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import { Action } from "../services/actions";
|
import { Action } from "../services/actions";
|
||||||
import { Env } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="o_navbar">
|
<div class="o_navbar">
|
||||||
<span class="title">Odoo</span>
|
<span class="title">Odoo</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li t-foreach="env.services.actions" t-as="action">
|
<li t-foreach="env.actions" t-as="action">
|
||||||
<a t-att-href="getUrl(action)"><t t-esc="action.title"/></a>
|
<a t-on-click="activateAction(action)" t-att-href="getUrl(action)">
|
||||||
|
<t t-esc="action.title"/>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,4 +23,9 @@ export default class Navbar extends Widget<Env> {
|
|||||||
const action_id = String(action.id);
|
const action_id = String(action.id);
|
||||||
return this.env.router.formatURL("web", { action_id });
|
return this.env.router.formatURL("web", { action_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activateAction(action: Action, event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.env.actionManager.doAction(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Widget from "../core/widget";
|
import Widget from "../core/widget";
|
||||||
import Navbar from "./navbar";
|
import Navbar from "./navbar";
|
||||||
import { Action } from "../services/actions";
|
import { Action } from "../services/actions";
|
||||||
import { Env } from "../types";
|
import { Env } from "../env";
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="o_web_client">
|
<div class="o_web_client">
|
||||||
@@ -39,9 +39,9 @@ export default class RootWidget extends Widget<Env> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAction(): Action {
|
getAction(): Action {
|
||||||
const routeInfo = this.env.router.getRouteInfo();
|
const routeInfo = this.env.router.getRoute();
|
||||||
const actionID = parseInt(routeInfo.query.action_id);
|
const actionID = parseInt(routeInfo.query.action_id);
|
||||||
let actions: Action[] = this.env.services.actions;
|
let actions: Action[] = this.env.actions;
|
||||||
let action = actions.find(a => a.id === actionID);
|
let action = actions.find(a => a.id === actionID);
|
||||||
if (!action) {
|
if (!action) {
|
||||||
action = actions.find(a => a.default === true);
|
action = actions.find(a => a.default === true);
|
||||||
|
|||||||
@@ -651,6 +651,21 @@ describe("t-on", () => {
|
|||||||
(<HTMLElement>node).click();
|
(<HTMLElement>node).click();
|
||||||
expect(a).toBe(6);
|
expect(a).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can bind handlers with loop variable as argument", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("test", `
|
||||||
|
<ul>
|
||||||
|
<li t-foreach="['someval']" t-as="action"><a t-on-click="activate(action)">link</a></li>
|
||||||
|
</ul>`);
|
||||||
|
const node = renderToDOM(qweb, "test", {
|
||||||
|
activate(action) {
|
||||||
|
expect(action).toBe('someval');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(<HTMLElement>node).getElementsByTagName('a')[0].click();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import Widget, {WidgetEnv} from "../src/ts/core/widget";
|
import Widget, {WEnv} from "../src/ts/core/widget";
|
||||||
import QWeb from "../src/ts/core/qweb_vdom";
|
import QWeb from "../src/ts/core/qweb_vdom";
|
||||||
import { Type } from "../src/ts/types";
|
|
||||||
|
|
||||||
|
|
||||||
type TestEnv = WidgetEnv;
|
interface Type<T> extends Function {
|
||||||
|
new (...args: any[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestEnv = WEnv;
|
||||||
type TestWidget = Widget<TestEnv>
|
type TestWidget = Widget<TestEnv>
|
||||||
|
|
||||||
function makeWidget(W: Type<TestWidget>): TestWidget {
|
function makeWidget(W: Type<TestWidget>): TestWidget {
|
||||||
|
|||||||
Reference in New Issue
Block a user