[REF] rework router into a class

This commit is contained in:
Géry Debongnie
2019-07-25 13:24:57 +02:00
parent ca920a0091
commit cd0762b436
9 changed files with 207 additions and 210 deletions
+2 -2
View File
@@ -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
View File
@@ -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() {
+165
View File
@@ -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;
}
+6 -6
View File
@@ -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);
}
-167
View File
@@ -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
View File
@@ -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;
+5 -7
View File
@@ -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
View File
@@ -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
});
});