mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] rework router into a class
This commit is contained in:
+2
-2
@@ -11,12 +11,12 @@ import { ConnectedComponent } from "./store/connected_component";
|
||||
import { Store } from "./store/store";
|
||||
import * as _utils from "./utils";
|
||||
import { Link } from "./router/Link";
|
||||
import { activate } from "./router/plugin";
|
||||
import { Router } from "./router/Router";
|
||||
|
||||
export { Component } from "./component/component";
|
||||
export { QWeb };
|
||||
export const core = { EventBus, Observer };
|
||||
export const router = { activate, Link };
|
||||
export const router = { Router, Link };
|
||||
export const store = { Store, ConnectedComponent };
|
||||
export const utils = _utils;
|
||||
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import { Component } from "../component/component";
|
||||
import { Destination, RouterEnv } from "./plugin";
|
||||
import { Destination, RouterEnv } from "./Router";
|
||||
|
||||
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
|
||||
export const LINK_TEMPLATE = `
|
||||
@@ -13,17 +13,17 @@ type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||
template = LINK_TEMPLATE_NAME;
|
||||
href: string = this.env.router.info.destToUrl(this.props);
|
||||
href: string = this.env.router.destToUrl(this.props);
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
this.href = this.env.router.info.destToUrl(nextProps);
|
||||
this.href = this.env.router.destToUrl(nextProps);
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
if (this.env.router.info.mode === "hash") {
|
||||
return document.location.hash === this.href;
|
||||
if (this.env.router.mode === "hash") {
|
||||
return (<any>document.location).hash === this.href;
|
||||
}
|
||||
return document.location.pathname === this.href;
|
||||
return (<any>document.location).pathname === this.href;
|
||||
}
|
||||
|
||||
navigate() {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Env } from "../component/component";
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { makeDirective } from "./directive";
|
||||
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
||||
|
||||
interface Route {
|
||||
name: string;
|
||||
path: string;
|
||||
component?: any;
|
||||
redirect?: Destination;
|
||||
params: string[];
|
||||
}
|
||||
|
||||
export type RouteParams = { [key: string]: string | number };
|
||||
|
||||
export interface RouterEnv extends Env {
|
||||
router: Router;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
to?: string;
|
||||
route?: string;
|
||||
params?: RouteParams;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
mode: Router["mode"];
|
||||
}
|
||||
|
||||
const paramRegexp = /\{\{(.*?)\}\}/;
|
||||
|
||||
export class Router {
|
||||
currentRoute: Route | null = null;
|
||||
currentParams: RouteParams | null = null;
|
||||
mode: "history" | "hash";
|
||||
|
||||
routes: { [id: string]: Route };
|
||||
routeIds: string[];
|
||||
env: RouterEnv;
|
||||
|
||||
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
|
||||
env.router = this;
|
||||
this.mode = options.mode;
|
||||
this.env = env as RouterEnv;
|
||||
|
||||
this.routes = {};
|
||||
this.routeIds = [];
|
||||
let nextId = 1;
|
||||
for (let partialRoute of routes) {
|
||||
if (!partialRoute.name) {
|
||||
partialRoute.name = "__route__" + nextId++;
|
||||
}
|
||||
if (partialRoute.component) {
|
||||
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
|
||||
}
|
||||
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
|
||||
this.routes[partialRoute.name] = partialRoute as Route;
|
||||
this.routeIds.push(partialRoute.name);
|
||||
}
|
||||
|
||||
this.checkRoute();
|
||||
|
||||
window.addEventListener("popstate", () => this.checkAndUpdateRoute());
|
||||
|
||||
// setup link and directive
|
||||
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
||||
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
||||
}
|
||||
|
||||
navigate(dest: Destination): void {
|
||||
const to = this.destToUrl(dest);
|
||||
history.pushState({}, to, location.origin + to);
|
||||
this.checkAndUpdateRoute();
|
||||
}
|
||||
|
||||
destToUrl(dest: Destination): string {
|
||||
return dest.to || this.routeToURL(this.routes[dest.route!].path, dest.params!);
|
||||
}
|
||||
|
||||
get currentRouteName(): string | null{
|
||||
return this.currentRoute && this.currentRoute.name;
|
||||
}
|
||||
|
||||
private routeToURL(path: string, params: RouteParams): string {
|
||||
const parts = path.split("/");
|
||||
const l = parts.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const part = parts[i];
|
||||
const match = part.match(paramRegexp);
|
||||
if (match) {
|
||||
const key = match[1].split(".")[0];
|
||||
parts[i] = <string>params[key];
|
||||
}
|
||||
}
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
private checkRoute(): void {
|
||||
let currentPath =
|
||||
this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
||||
currentPath = currentPath || "/";
|
||||
for (let routeId of this.routeIds) {
|
||||
let route = this.routes[routeId];
|
||||
let params = this.matchRoute(route.path, currentPath);
|
||||
if (params) {
|
||||
this.currentRoute = route;
|
||||
this.currentParams = params;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.currentRoute = null;
|
||||
this.currentParams = {};
|
||||
}
|
||||
|
||||
private checkAndUpdateRoute(): void {
|
||||
const initialName = this.currentRoute ? this.currentRoute.name : null;
|
||||
this.checkRoute();
|
||||
const currentName = this.currentRoute ? this.currentRoute.name : null;
|
||||
|
||||
if (currentName !== initialName) {
|
||||
this.env.qweb.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private matchRoute(routePath: string, path: string): RouteParams | false {
|
||||
if (routePath === "*") {
|
||||
return {};
|
||||
}
|
||||
const descrParts = routePath.split("/");
|
||||
const targetParts = path.split("/");
|
||||
const l = descrParts.length;
|
||||
if (l !== targetParts.length) {
|
||||
return false;
|
||||
}
|
||||
const result = {};
|
||||
for (let i = 0; i < l; i++) {
|
||||
const descr = descrParts[i];
|
||||
let target: string | number = targetParts[i];
|
||||
const match = descr.match(paramRegexp);
|
||||
if (match) {
|
||||
const [key, suffix] = match[1].split(".");
|
||||
if (suffix === "number") {
|
||||
target = parseInt(target, 10);
|
||||
}
|
||||
result[key] = target;
|
||||
} else if (descr !== target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function findParams(str: string): string[] {
|
||||
const globalParamRegexp = /\{\{(.*?)\}\}/g;
|
||||
const result: string[] = [];
|
||||
let m;
|
||||
do {
|
||||
m = globalParamRegexp.exec(str);
|
||||
if (m) {
|
||||
result.push(m[1].split(".")[0]);
|
||||
}
|
||||
} while (m);
|
||||
return result;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouterEnv } from "./plugin";
|
||||
import { RouterEnv } from "./Router";
|
||||
|
||||
export function makeDirective(env: RouterEnv) {
|
||||
return {
|
||||
@@ -6,17 +6,17 @@ export function makeDirective(env: RouterEnv) {
|
||||
priority: 13,
|
||||
atNodeEncounter({ node }): boolean {
|
||||
let first = true;
|
||||
const info = env.router.info;
|
||||
for (let name of info.routeIds) {
|
||||
const route = info.routes[name];
|
||||
const router = env.router;
|
||||
for (let name of router.routeIds) {
|
||||
const route = router.routes[name];
|
||||
if (route.component) {
|
||||
// make new t t-component element
|
||||
const comp = node.ownerDocument.createElement("t");
|
||||
comp.setAttribute("t-component", "__component__" + route.name);
|
||||
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.routeName === '${route.name}'`);
|
||||
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
|
||||
first = false;
|
||||
for (let param of route.params) {
|
||||
comp.setAttribute(param, `env.router.routeParams.${param}`);
|
||||
comp.setAttribute(param, `env.router.currentParams.${param}`);
|
||||
}
|
||||
node.appendChild(comp);
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { Env } from "../component/component";
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { makeDirective } from "./directive";
|
||||
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
||||
|
||||
interface Route {
|
||||
name?: string;
|
||||
path: string;
|
||||
component?: any;
|
||||
}
|
||||
|
||||
export interface RouteDescription extends Route {
|
||||
name: string;
|
||||
params: string[];
|
||||
}
|
||||
|
||||
export type RouteParams = { [key: string]: string | number };
|
||||
|
||||
export interface RouterEnv extends Env {
|
||||
router: Router;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
to?: string;
|
||||
route?: string;
|
||||
params?: RouteParams;
|
||||
}
|
||||
interface Router {
|
||||
navigate: (dest: Destination) => void;
|
||||
routeName: string | null;
|
||||
routeParams: RouteParams;
|
||||
info: RouterInfo;
|
||||
}
|
||||
|
||||
interface RouterInfo {
|
||||
mode: Options["mode"];
|
||||
routes: { [id: string]: RouteDescription };
|
||||
routeIds: string[];
|
||||
destToUrl: (dest: Destination) => string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
mode: "history" | "hash";
|
||||
}
|
||||
|
||||
const paramRegexp = /\{\{(.*?)\}\}/;
|
||||
|
||||
function findParams(str: string): string[] {
|
||||
const globalParamRegexp = /\{\{(.*?)\}\}/g;
|
||||
const result: string[] = [];
|
||||
let m;
|
||||
do {
|
||||
m = globalParamRegexp.exec(str);
|
||||
if (m) {
|
||||
result.push(m[1].split('.')[0]);
|
||||
}
|
||||
} while (m);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function activate(env: Env, routes: Route[], options?: Options) {
|
||||
// process routes and build proper internal data structures
|
||||
const mode = options ? options.mode : "hash";
|
||||
const info: RouterInfo = { routes: {}, routeIds: [], mode, destToUrl: destToURL.bind(null, env) };
|
||||
let nextId = 1;
|
||||
for (let route of routes) {
|
||||
if (!route.name) {
|
||||
route.name = "__route__" + nextId++;
|
||||
}
|
||||
if (route.component) {
|
||||
QWeb.register("__component__" + route.name, route.component);
|
||||
}
|
||||
(<RouteDescription>route).params = findParams(route.path);
|
||||
info.routes[route.name] = <RouteDescription>route;
|
||||
info.routeIds.push(route.name);
|
||||
}
|
||||
|
||||
const router: Router = { navigate, routeName: null, routeParams: {}, info };
|
||||
|
||||
env.router = router;
|
||||
|
||||
checkRoute(<RouterEnv>env);
|
||||
|
||||
function navigate(dest: Destination) {
|
||||
const to = destToURL(<RouterEnv>env, dest);
|
||||
history.pushState({}, to, location.origin + to);
|
||||
checkAndUpdateRoute(<RouterEnv>env);
|
||||
}
|
||||
|
||||
addEventListener("popstate", () => checkAndUpdateRoute(<RouterEnv>env));
|
||||
|
||||
// setup link and directive
|
||||
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
||||
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
||||
}
|
||||
|
||||
function checkRoute(env: RouterEnv): void {
|
||||
const info = env.router.info;
|
||||
let currentPath =
|
||||
info.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
||||
currentPath = currentPath || "/";
|
||||
for (let routeId of info.routeIds) {
|
||||
let route = info.routes[routeId];
|
||||
let params = matchRoute(route, currentPath);
|
||||
if (params) {
|
||||
env.router.routeName = route.name!;
|
||||
env.router.routeParams = params;
|
||||
return;
|
||||
}
|
||||
}
|
||||
env.router.routeName = null;
|
||||
env.router.routeParams = {};
|
||||
}
|
||||
|
||||
function checkAndUpdateRoute(env: RouterEnv): void {
|
||||
const currentRoute = env.router.routeName;
|
||||
checkRoute(env);
|
||||
if (env.router.routeName !== currentRoute) {
|
||||
env.qweb.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export function matchRoute(route: Route, path: string): RouteParams | false {
|
||||
if (route.path === "*") {
|
||||
return {};
|
||||
}
|
||||
const descrParts = route.path.split("/");
|
||||
const targetParts = path.split("/");
|
||||
const l = descrParts.length;
|
||||
if (l !== targetParts.length) {
|
||||
return false;
|
||||
}
|
||||
const result = {};
|
||||
for (let i = 0; i < l; i++) {
|
||||
const descr = descrParts[i];
|
||||
let target: string | number = targetParts[i];
|
||||
const match = descr.match(paramRegexp);
|
||||
if (match) {
|
||||
const [key, suffix] = match[1].split(".");
|
||||
if (suffix === "number") {
|
||||
target = parseInt(target, 10);
|
||||
}
|
||||
result[key] = target;
|
||||
} else if (descr !== target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function destToURL(env: RouterEnv, dest: Destination): string {
|
||||
return dest.to || routeToURL(env.router.info.routes[dest.route!].path, dest.params!);
|
||||
}
|
||||
|
||||
export function routeToURL(path: string, params: RouteParams): string {
|
||||
const parts = path.split("/");
|
||||
const l = parts.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const part = parts[i];
|
||||
const match = part.match(paramRegexp);
|
||||
if (match) {
|
||||
const key = match[1].split(".")[0];
|
||||
parts[i] = <string>params[key];
|
||||
}
|
||||
}
|
||||
return parts.join("/");
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* - debounce
|
||||
*/
|
||||
|
||||
export function whenReady(fn) {
|
||||
export function whenReady(fn?: any) {
|
||||
return new Promise(function(resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve();
|
||||
|
||||
@@ -10,13 +10,13 @@ exports[`router directive t-routecomponent can render parameterized route 1`] =
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.routeName==='book') {
|
||||
if (context['env'].router.currentRouteName==='book') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {title:context['env'].router.routeParams.title};
|
||||
let props4 = {title:context['env'].router.currentParams.title};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
@@ -53,13 +53,13 @@ exports[`router directive t-routecomponent can render parameterized route with s
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.routeName==='book') {
|
||||
if (context['env'].router.currentRouteName==='book') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {title:context['env'].router.routeParams.title,val:context['env'].router.routeParams.val};
|
||||
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
@@ -96,7 +96,7 @@ exports[`router directive t-routecomponent can render simple cases 1`] = `
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.routeName==='about') {
|
||||
if (context['env'].router.currentRouteName==='about') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
@@ -125,7 +125,7 @@ exports[`router directive t-routecomponent can render simple cases 1`] = `
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
}
|
||||
else if (context['env'].router.routeName==='users') {
|
||||
else if (context['env'].router.currentRouteName==='users') {
|
||||
//COMPONENT
|
||||
let def6;
|
||||
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "../../src/component/component";
|
||||
import { QWeb } from "../../src/qweb/index";
|
||||
import { activate, RouterEnv } from "../../src/router/plugin";
|
||||
import { Router, RouterEnv } from "../../src/router/Router";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
|
||||
describe("router directive t-routecomponent", () => {
|
||||
@@ -41,8 +41,8 @@ describe("router directive t-routecomponent", () => {
|
||||
{ name: "about", path: "/about", component: About },
|
||||
{ name: "users", path: "/users", component: Users }
|
||||
];
|
||||
activate(env, routes, {mode: 'history'});
|
||||
const router = env.router;
|
||||
|
||||
const router = new Router(env,routes, {mode: 'history'})
|
||||
router.navigate({ route: "about" });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
@@ -69,8 +69,7 @@ describe("router directive t-routecomponent", () => {
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||
activate(env, routes, {mode: 'history'});
|
||||
const router = env.router;
|
||||
const router = new Router(env,routes, {mode: 'history'})
|
||||
router.navigate({ route: "book", params: { title: "1984" } });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
@@ -97,8 +96,7 @@ describe("router directive t-routecomponent", () => {
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||
activate(env, routes, {mode: 'history'});
|
||||
const router = env.router;
|
||||
const router = new Router(env,routes, {mode: 'history'})
|
||||
router.navigate({ route: "book", params: { title: "1984", val: "123" } });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
|
||||
+16
-15
@@ -1,38 +1,39 @@
|
||||
import { matchRoute, routeToURL } from "../../src/router/plugin";
|
||||
import { Router } from "../../src/router/Router";
|
||||
|
||||
describe("routeToURL", () => {
|
||||
test("simple non parameterized path", () => {
|
||||
expect(routeToURL("/abc", {})).toBe("/abc");
|
||||
expect(routeToURL("/abc/def", {})).toBe("/abc/def");
|
||||
expect(routeToURL("/abc", {val: 12})).toBe("/abc");
|
||||
});
|
||||
|
||||
test("simple parameterized path", () => {
|
||||
expect(routeToURL("/abc/{{def}}", {def: 34})).toBe("/abc/34");
|
||||
});
|
||||
const routeToURL = Router.prototype.routeToURL;
|
||||
test("simple non parameterized path", () => {
|
||||
expect(routeToURL("/abc", {})).toBe("/abc");
|
||||
expect(routeToURL("/abc/def", {})).toBe("/abc/def");
|
||||
expect(routeToURL("/abc", { val: 12 })).toBe("/abc");
|
||||
});
|
||||
|
||||
test("simple parameterized path", () => {
|
||||
expect(routeToURL("/abc/{{def}}", { def: 34 })).toBe("/abc/34");
|
||||
});
|
||||
});
|
||||
|
||||
describe("match routes", () => {
|
||||
const matchRoute = Router.prototype.matchRoute;
|
||||
test("properly match simple routes", () => {
|
||||
// simple route
|
||||
expect(matchRoute({ path: "/home", name: "someroute" }, "/home")).toEqual({});
|
||||
expect(matchRoute("/home", "/home")).toEqual({});
|
||||
|
||||
// no match
|
||||
expect(matchRoute({ path: "/home", name: "someroute" }, "/otherpath")).toEqual(false);
|
||||
expect(matchRoute("/home", "/otherpath")).toEqual(false);
|
||||
|
||||
// fallback route
|
||||
expect(matchRoute({ path: "*", name: "someroute" }, "somepath")).toEqual({});
|
||||
expect(matchRoute("*", "somepath")).toEqual({});
|
||||
});
|
||||
|
||||
test("match some parameterized routes", () => {
|
||||
expect(matchRoute({ path: "/invoices/{{id}}", name: "someroute" }, "/invoices/3")).toEqual({
|
||||
expect(matchRoute("/invoices/{{id}}", "/invoices/3")).toEqual({
|
||||
id: "3"
|
||||
});
|
||||
});
|
||||
|
||||
test("can convert to number if needed", () => {
|
||||
expect(matchRoute({ path: "/invoices/{{id.number}}", name: "someroute" }, "/invoices/3")).toEqual({
|
||||
expect(matchRoute("/invoices/{{id.number}}", "/invoices/3")).toEqual({
|
||||
id: 3
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user