From 97621dd919948ca6a4847b2f6d85b1febfa3aab4 Mon Sep 17 00:00:00 2001 From: Elmi Ahmadov Date: Wed, 4 Mar 2020 17:38:38 +0100 Subject: [PATCH 1/2] [WIP] Router API --- src/api/router/Route.ts | 24 +++++++ src/api/router/Router.ts | 115 ++++++++++++++++++++++++++++++++ src/api/router/RouterHistory.ts | 28 ++++++++ src/api/router/RouterMatcher.ts | 30 +++++++++ src/index.ts | 2 + 5 files changed, 199 insertions(+) create mode 100644 src/api/router/Route.ts create mode 100644 src/api/router/Router.ts create mode 100644 src/api/router/RouterHistory.ts create mode 100644 src/api/router/RouterMatcher.ts diff --git a/src/api/router/Route.ts b/src/api/router/Route.ts new file mode 100644 index 0000000..9941c3b --- /dev/null +++ b/src/api/router/Route.ts @@ -0,0 +1,24 @@ +import { Page, Properties } from "tabris"; + +export interface RouteOptions { + enableDrawer?: boolean; + toolbarVisible?: boolean; +} + +export interface RoutePage { + onPayload?(payload?: any): void; +} + +export abstract class RoutePage extends Page { + constructor(properties?: Properties) { + super(properties); + } +} + +export abstract class Route { + page: RoutePage; + options: RouteOptions = { + enableDrawer: false, + toolbarVisible: true + } +} diff --git a/src/api/router/Router.ts b/src/api/router/Router.ts new file mode 100644 index 0000000..e62f1a2 --- /dev/null +++ b/src/api/router/Router.ts @@ -0,0 +1,115 @@ +import { NavigationView } from "tabris"; +import { ListLike, Mutation } from "../List"; +import { RouterMatcher } from "./RouterMatcher"; +import { RouterHistory, HistoryItem } from "./RouterHistory"; +import { Route } from "./Route"; +import { Constructor } from "../../internals/utils"; + +export type RouterConfig = { + name: string; + route: Constructor +}; + +export type RouterProperties = { + navigationView: NavigationView, + routers?: ListLike, + defaultRoute?: HistoryItem, + history?: ListLike +}; + +export class Router { + + private _navigationView: NavigationView; + private _routes: ListLike; + + private _routerHistoryObserver: RouterHistory; + private _routerMatcher: RouterMatcher; + + constructor({navigationView, routers, defaultRoute, history} : RouterProperties) { + this._navigationView = navigationView; + this.routes = routers || []; + this._routerHistoryObserver = new RouterHistory(this._handleHistoryChange); + this.history = history || []; + this._routerMatcher = new RouterMatcher(this); + if (defaultRoute) { + this.goTo(defaultRoute as ItemType); + } + this._navigationView.onRemoveChild(this._syncHistoryWithNavigationView.bind(this)); + } + + goTo(item: ItemType) { + this._routerHistoryObserver.push(item); + } + + back() { + if (this._routerHistoryObserver.source.length === 0) { + throw new Error("Could not call back on empty history stack"); + } + this._routerHistoryObserver.pop(); + } + + set history(value: ListLike) { + if (this._routerHistoryObserver.source === value) { + return; + } + this._routerHistoryObserver.source = value; + } + + get history() { + return this._routerHistoryObserver.source; + } + + set routes(value: ListLike) { + if (this._routes === value) { + return; + } + this._routes = value; + this._routerMatcher = new RouterMatcher(this); + } + + get routes() { + return this._routes; + } + + protected _handleHistoryChange = ({deleteCount, items}: Mutation) => { + if (deleteCount > items.length) { + this._disposeRoutes(deleteCount); + } else if (items.length > deleteCount) { + this._appendRoutes(items); + } + } + + private _disposeRoutes(count: number = 0) { + if (count <= 0) { + return; + } + const size = this._navigationView.children().length; + this._navigationView + .children() + .slice(size - count) + .forEach(child => child.dispose()); + } + + private _appendRoutes(routes: ListLike) { + routes.forEach(item => { + const route = this._routerMatcher.match(item); + this._appendRoute(route, item.payload); + }); + } + + private _appendRoute(route: Route, payload?: any) { + if (route.page.onPayload && typeof route.page.onPayload === 'function') { + route.page.onPayload(payload); + } + this._navigationView.append(route.page); + this._navigationView.drawerActionVisible = route.options.enableDrawer; + this._navigationView.toolbarVisible = route.options.toolbarVisible; + } + + private _syncHistoryWithNavigationView() { + while (this.history.length !== this._navigationView.children().length) { + this._routerHistoryObserver.removeLast(); + } + } + +} diff --git a/src/api/router/RouterHistory.ts b/src/api/router/RouterHistory.ts new file mode 100644 index 0000000..02efee7 --- /dev/null +++ b/src/api/router/RouterHistory.ts @@ -0,0 +1,28 @@ +import { ListLikeObvserver } from "../../internals/ListLikeObserver"; + +export type HistoryItem = { route: string, payload?: any }; + +export class RouterHistory extends ListLikeObvserver { + + public push(item: T) { + const source = Array.from(this.source); + source.push(item); + this.source = source; + } + + public pop(): T { + const source = Array.from(this.source); + const result = source.pop(); + this.source = source; + return result; + } + + public removeLast() { + return this.source.pop(); + } + + get current() { + return this.source[this.source.length - 1]; + } + +} diff --git a/src/api/router/RouterMatcher.ts b/src/api/router/RouterMatcher.ts new file mode 100644 index 0000000..28a8410 --- /dev/null +++ b/src/api/router/RouterMatcher.ts @@ -0,0 +1,30 @@ +import { Router, RouterConfig } from "./Router"; +import { Route } from "./Route"; +import { HistoryItem } from './RouterHistory'; +import { Constructor } from "../../internals/utils"; + +export class RouterMatcher { + private _nameMap: Map = new Map(); + + constructor(router: Router) { + const routes = router.routes || []; + routes.forEach(item => { + if (this._nameMap.has(item.name)) { + throw new Error(`Route with '${item.name}' name already exists!`); + } + this._nameMap.set(item.name, item); + }); + } + + match(historyItem: HistoryItem): Route { + const name = historyItem.route; + if (this._nameMap.has(name)) { + return this._createRoute(this._nameMap.get(name).route); + } + throw new Error(`Route with '${name}' name does not exist!`); + } + + private _createRoute(className: Constructor): Route { + return new className(); // TODO: support to pass parameters + } +} diff --git a/src/index.ts b/src/index.ts index d186d08..2ae25b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,8 @@ export * from './api/to'; export * from './api/List'; export * from './api/ListView'; export * from './api/Cell'; +export * from './api/router/Route'; +export * from './api/router/Router'; /** * A decorator that marks a constructor parameter for injections based on the type of the parameter: From 8664c1d1ddd0994b4ea1de989f44dcd50d4133ab Mon Sep 17 00:00:00 2001 From: Elmi Ahmadov Date: Fri, 6 Mar 2020 15:03:37 +0100 Subject: [PATCH 2/2] Router: add example --- examples/router/README.md | 7 ++++ examples/router/package.json | 21 +++++++++++ examples/router/src/app.tsx | 64 +++++++++++++++++++++++++++++++++ examples/router/tsconfig.json | 17 +++++++++ src/api/router/Route.ts | 26 +++----------- src/api/router/Router.ts | 50 +++++++------------------- src/api/router/RouterHistory.ts | 41 ++++++++++++++++----- src/api/router/RouterMatcher.ts | 26 ++++---------- 8 files changed, 166 insertions(+), 86 deletions(-) create mode 100644 examples/router/README.md create mode 100644 examples/router/package.json create mode 100644 examples/router/src/app.tsx create mode 100644 examples/router/tsconfig.json diff --git a/examples/router/README.md b/examples/router/README.md new file mode 100644 index 0000000..04446a5 --- /dev/null +++ b/examples/router/README.md @@ -0,0 +1,7 @@ +# Example "router" + +[![GitPod Logo](../../doc/run-in-gitpod.png)](https://gitpod.io/#example=router/https://github.com/eclipsesource/tabris-decorators/tree/tabris-router/examples/router) + +## Description + +Demonstrates the usage of the router API. diff --git a/examples/router/package.json b/examples/router/package.json new file mode 100644 index 0000000..3d6e2fd --- /dev/null +++ b/examples/router/package.json @@ -0,0 +1,21 @@ +{ + "name": "router", + "version": "3.3.0", + "dependencies": { + "reflect-metadata": "^0.1.13" + }, + "optionalDependencies": { + "tabris": "^3.3.0", + "tabris-decorators": "3.3.0" + }, + "devDependencies": { + "typescript": "3.3.x" + }, + "main": "dist/app.js", + "scripts": { + "start": "tabris serve -w -a", + "build": "tsc -p .", + "watch": "tsc -p . -w --preserveWatchOutput --inlineSourceMap", + "gitpod": "tabris serve -a -w --no-intro --port 8080 --external $(gp url 8080):443" + } +} diff --git a/examples/router/src/app.tsx b/examples/router/src/app.tsx new file mode 100644 index 0000000..753a937 --- /dev/null +++ b/examples/router/src/app.tsx @@ -0,0 +1,64 @@ +import {NavigationView, contentView, Button, TextView, TextInput, Stack, Page, Properties} from 'tabris'; +import {Router, Route, injectable, create, resolve, property, component } from 'tabris-decorators'; + +const navigationView = new NavigationView({ + layoutData: 'stretch' +}).appendTo(contentView); + +class MyPage1 extends Page { + constructor(properties?: Properties) { + super(properties); + this.append( + + +