diff --git a/jest.config.js b/jest.config.js index 53234731..95e94116 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - roots: ["/web/static", "/tests"], + roots: ["/web/static"], transform: { "^.+\\.ts?$": "ts-jest" }, diff --git a/package.json b/package.json index 0881ccda..876b8b6f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "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: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/", "predev": "npm run build", "dev": "npm-run-all --parallel \"build:* -- --watch\" serve" @@ -34,7 +35,8 @@ "snabbdom-to-html": "^5.1.1", "source-map-support": "^0.5.10", "ts-jest": "^23.10.5", - "typescript": "^3.2.2" + "typescript": "^3.2.2", + "uglify-es": "^3.3.9" }, "dependencies": { "snabbdom": "^0.7.3" diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/Widget.ts index 32d05746..c9a53d64 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/Widget.ts @@ -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 sdListeners from "../../../libs/snabbdom/src/modules/eventlisteners"; +import { init } from "../../../libs/snabbdom/src/snabbdom"; import { VNode } from "../../../libs/snabbdom/src/vnode"; import QWeb from "./qweb_vdom"; const patch = init([sdListeners, sdAttrs]); -export interface WidgetEnv { +export interface WEnv { qweb: QWeb; } -export default class Widget { +export default class Widget { name: string = "widget"; template: string = "
"; vnode: VNode | null = null; @@ -49,7 +49,7 @@ export default class Widget { async mount(target?: HTMLElement): Promise { await this.willStart(); - this.env!.qweb.addTemplate(this.name, this.template); + this.env.qweb.addTemplate(this.name, this.template); delete this.template; const vnode = await this.render(); diff --git a/web/static/src/ts/core/qweb_vdom.ts b/web/static/src/ts/core/qweb_vdom.ts index 5a0ff32a..5f1ba7e5 100644 --- a/web/static/src/ts/core/qweb_vdom.ts +++ b/web/static/src/ts/core/qweb_vdom.ts @@ -640,7 +640,7 @@ const forEachDirective: Directive = { const onDirective: Directive = { name: "on", priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { + atNodeCreation({ ctx, fullName, value, nodeID, qweb }) { const eventName = fullName.slice(5); let extraArgs; let handler = value.replace(/\(.*\)/, function(args) { @@ -649,7 +649,7 @@ const onDirective: Directive = { }); ctx.addLine( `p${nodeID}.on = {${eventName}: context['${handler}'].bind(context${ - extraArgs ? ", " + extraArgs : "" + extraArgs ? ", " + qweb._formatExpression(extraArgs) : "" })}` ); } diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts index 5ac8fe4e..be36a0e4 100644 --- a/web/static/src/ts/env.ts +++ b/web/static/src/ts/env.ts @@ -1,12 +1,29 @@ 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 actions from "./services/actions"; -import { Env } from "./types"; -const env: Env = { - qweb: new QWeb(), - router: new Router(), - services: { actions } -}; +export interface Env extends WEnv { + actionManager: ActionManager; + actions: Action[]; + 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; diff --git a/web/static/src/ts/main.ts b/web/static/src/ts/main.ts index d2f14022..0b09e01c 100644 --- a/web/static/src/ts/main.ts +++ b/web/static/src/ts/main.ts @@ -1,9 +1,10 @@ /// import RootWidget from "./widgets/root_widget"; -import env from "./env"; +import {makeEnvironment} from "./env"; document.addEventListener("DOMContentLoaded", async function() { + const env = makeEnvironment(); const rootWidget = new RootWidget(env); await rootWidget.mount(document.body); }); diff --git a/web/static/src/ts/services/action_manager.ts b/web/static/src/ts/services/action_manager.ts new file mode 100644 index 00000000..aa686685 --- /dev/null +++ b/web/static/src/ts/services/action_manager.ts @@ -0,0 +1,24 @@ +import { Action } from "./actions"; + +// export interface Action { +// id: number; +// title: string; +// Widget: Type>; +// 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 + } +} \ No newline at end of file diff --git a/web/static/src/ts/services/actions.ts b/web/static/src/ts/services/actions.ts index b453f1df..17e3c252 100644 --- a/web/static/src/ts/services/actions.ts +++ b/web/static/src/ts/services/actions.ts @@ -1,9 +1,11 @@ import CRM from "../widgets/crm"; import Discuss from "../widgets/discuss"; import Widget from "../core/widget"; -import { Env, Type } from "../types"; - +import { Env } from "../env"; +interface Type extends Function { + new (...args: any[]): T; +} export interface Action { id: number; @@ -19,3 +21,9 @@ const actions: Action[] = [ export default actions; +// class ActionManager { + +// doAction(action: Action) { + +// } +// } diff --git a/web/static/src/ts/services/ajax.ts b/web/static/src/ts/services/ajax.ts new file mode 100644 index 00000000..c5214afa --- /dev/null +++ b/web/static/src/ts/services/ajax.ts @@ -0,0 +1,2 @@ + +export default class Ajax{} \ No newline at end of file diff --git a/web/static/src/ts/services/router.ts b/web/static/src/ts/services/router.ts index b8c4d71e..bba7da5b 100644 --- a/web/static/src/ts/services/router.ts +++ b/web/static/src/ts/services/router.ts @@ -1,39 +1,46 @@ -export type Route = string; export type Query = { [key: string]: string }; -export interface RouteInfo { - route: Route; +export interface Route { + // this is the part before the hash: www.something.com/web#action=1 => web + path: string; query: Query; title: string; } +function clearSlaches(s: string): string { + return s.replace(/\/$/, "").replace(/^\//, ""); +} + export default class Router { - listeners: { owner: any; callback: (info: RouteInfo) => void }[] = []; + listeners: { owner: any; callback: (info: Route) => void }[] = []; constructor() { window.addEventListener("popstate", this.onUrlChange.bind(this)); + window.onhashchange = function () { console.log('aaaa'); debugger; } } - onUrlChange() { - const info = this.getRouteInfo(); - for (let listener of this.listeners) { - listener.callback.call(listener.owner, info); - } + onUrlChange(event: PopStateEvent) { + debugger + event.preventDefault(); + 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 {Query} query */ - navigate(info: Partial) { - const currentRouteInfo = this.getRouteInfo(); - const route = info.route || currentRouteInfo.route; + navigate(info: Partial) { + const currentRoute = this.getRoute(); + const route = info.path || currentRoute.path; const query = info.query || {}; - const title = info.title || currentRouteInfo.title; + const title = info.title || currentRoute.title; const url = this.formatURL(route, query); 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 }); } @@ -41,24 +48,24 @@ export default class Router { this.listeners = this.listeners.filter(l => l.owner !== owner); } - formatURL(route: Route, query: Query): string { - let url = route; + formatURL(path: string, query: Query): string { + let url = clearSlaches(path); let hasHash = false; for (let key in query) { url = url + (hasHash ? "&" : "#"); url = url + `${key}=${query[key]}`; hasHash = true; } - return url; + return "/" + url; } - getRouteInfo(): RouteInfo { - const route = window.location.pathname.slice(1); + getRoute(): Route { + const path = clearSlaches(window.location.pathname); 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 }; + return { path, query, title: document.title }; } } diff --git a/web/static/src/ts/types.ts b/web/static/src/ts/types.ts deleted file mode 100644 index 4337398d..00000000 --- a/web/static/src/ts/types.ts +++ /dev/null @@ -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 extends Function { - new (...args: any[]): T; -} diff --git a/web/static/src/ts/widgets/CRM.ts b/web/static/src/ts/widgets/CRM.ts index 823e045a..9a2670a4 100644 --- a/web/static/src/ts/widgets/CRM.ts +++ b/web/static/src/ts/widgets/CRM.ts @@ -1,5 +1,5 @@ import Widget from "../core/widget"; -import { Env } from "../types"; +import { Env } from "../env"; const template = `
diff --git a/web/static/src/ts/widgets/Counter.ts b/web/static/src/ts/widgets/Counter.ts index fa7b5cde..8c10202b 100644 --- a/web/static/src/ts/widgets/Counter.ts +++ b/web/static/src/ts/widgets/Counter.ts @@ -1,5 +1,5 @@ import Widget from "../core/widget"; -import { Env } from "../types"; +import { Env } from "../env"; const template = `
diff --git a/web/static/src/ts/widgets/Discuss.ts b/web/static/src/ts/widgets/Discuss.ts index dbc2535e..54741af4 100644 --- a/web/static/src/ts/widgets/Discuss.ts +++ b/web/static/src/ts/widgets/Discuss.ts @@ -1,6 +1,6 @@ import Widget from "../core/widget"; import Counter from "./counter"; -import { Env } from "../types"; +import { Env } from "../env"; const template = `
diff --git a/web/static/src/ts/widgets/Navbar.ts b/web/static/src/ts/widgets/Navbar.ts index de5ea12a..040bbd7c 100644 --- a/web/static/src/ts/widgets/Navbar.ts +++ b/web/static/src/ts/widgets/Navbar.ts @@ -1,13 +1,15 @@ import Widget from "../core/widget"; import { Action } from "../services/actions"; -import { Env } from "../types"; +import { Env } from "../env"; const template = `
Odoo
@@ -21,4 +23,9 @@ export default class Navbar extends Widget { const action_id = String(action.id); return this.env.router.formatURL("web", { action_id }); } + + activateAction(action: Action, event: MouseEvent) { + event.preventDefault(); + this.env.actionManager.doAction(action); + } } diff --git a/web/static/src/ts/widgets/root_widget.ts b/web/static/src/ts/widgets/root_widget.ts index a2b042ba..3c541522 100644 --- a/web/static/src/ts/widgets/root_widget.ts +++ b/web/static/src/ts/widgets/root_widget.ts @@ -1,7 +1,7 @@ import Widget from "../core/widget"; import Navbar from "./navbar"; import { Action } from "../services/actions"; -import { Env } from "../types"; +import { Env } from "../env"; const template = `
@@ -39,9 +39,9 @@ export default class RootWidget extends Widget { } getAction(): Action { - const routeInfo = this.env.router.getRouteInfo(); + const routeInfo = this.env.router.getRoute(); 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); if (!action) { action = actions.find(a => a.default === true); diff --git a/web/static/tests/qweb_vdom.test.ts b/web/static/tests/qweb_vdom.test.ts index e4fe75ce..4c2294da 100644 --- a/web/static/tests/qweb_vdom.test.ts +++ b/web/static/tests/qweb_vdom.test.ts @@ -651,6 +651,21 @@ describe("t-on", () => { (node).click(); expect(a).toBe(6); }); + + test("can bind handlers with loop variable as argument", () => { + expect.assertions(1); + const qweb = new QWeb(); + qweb.addTemplate("test", ` + `); + const node = renderToDOM(qweb, "test", { + activate(action) { + expect(action).toBe('someval'); + } + }); + (node).getElementsByTagName('a')[0].click(); + }); }); diff --git a/web/static/tests/widget.test.ts b/web/static/tests/widget.test.ts index de7d8672..d35557fe 100644 --- a/web/static/tests/widget.test.ts +++ b/web/static/tests/widget.test.ts @@ -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 { Type } from "../src/ts/types"; -type TestEnv = WidgetEnv; +interface Type extends Function { + new (...args: any[]): T; +} + +type TestEnv = WEnv; type TestWidget = Widget function makeWidget(W: Type): TestWidget {