Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabris.js router #34

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/router/README.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions examples/router/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
64 changes: 64 additions & 0 deletions examples/router/src/app.tsx
Original file line number Diff line number Diff line change
@@ -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<MyPage1>) {
super(properties);
this.append(
<Stack stretch alignment="stretchX" padding={[0, 4]}>
<TextInput/>
<Button text="Open" onSelect={() =>
router.goTo({
route: 'MyRoute2',
payload: {
text: this.find(TextInput).only().text
}
})}
/>
</Stack>
);
}
}

@component
class MyPage2 extends Page {
@property text: string;

constructor(properties?: Properties<MyPage2>) {
super(properties);
this.append(
<Stack stretch alignment="stretchX" padding={[0, 4]}>
<TextView alignment="centerX" height={32} bind-text="text"/>
<Button text="Go back" onSelect={() => router.back()}/>
<TextInput bind-text="text" />
<Button text="Open" onSelect={() =>
router.goTo({
route: 'MyRoute2',
payload: {
text: this._find(TextInput).only().text
}
})}
/>
</Stack>
);
}
}

@injectable({ param: 'MyRoute1' })
class MyRoute1 extends Route {
page = new MyPage1({title: 'foo'});
};

@injectable({ param: 'MyRoute2' })
class MyRoute2 extends Route {
page = new MyPage2({title: 'bar'});
};

const router = new Router({
navigationView,
history: [{ route: 'MyRoute1' }]
});
17 changes: 17 additions & 0 deletions examples/router/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": ["es6" ],
"jsx": "react",
"jsxFactory": "JSX.createElement",
"outDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["./node_modules/@types"]
},
"include": [
"./src/*.ts",
"./src/*.tsx"
]
}
8 changes: 8 additions & 0 deletions src/api/router/Route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Page } from "tabris";

export type Dictionary<T> = { [key: string]: T };

export class Route<PayloadType = Dictionary<string>> {
page: Page;
payload?: PayloadType;
}
89 changes: 89 additions & 0 deletions src/api/router/Router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { NavigationView } from "tabris";
import { ListLike, Mutation } from "../List";
import { RouterMatcher } from "./RouterMatcher";
import { RouterHistory, HistoryItem } from "./RouterHistory";
import { Route } from "./Route";

export type RouterProperties = {
navigationView: NavigationView,
defaultRoute?: HistoryItem,
history?: ListLike<HistoryItem>
};

export class Router<ItemType extends HistoryItem = HistoryItem> {

private _navigationView: NavigationView;
private _routerHistoryObserver: RouterHistory;
private _routerMatcher: RouterMatcher;

constructor({navigationView, history} : RouterProperties) {
this._navigationView = navigationView;
this._routerHistoryObserver = new RouterHistory(this._handleHistoryChange);
this._routerMatcher = new RouterMatcher();
this.history = history || [];
this._navigationView.onRemoveChild(this._syncHistoryWithNavigationView.bind(this));
}

goTo(item: ItemType) {
this._routerHistoryObserver.push(item);
}

back() {
if (this._routerHistoryObserver.history.length === 0) {
throw new Error("Could not call back on empty history stack");
}
this._routerHistoryObserver.pop();
}

set history(value: ListLike<HistoryItem>) {
this._routerHistoryObserver.history = value;
}

get history() {
return this._routerHistoryObserver.history;
}

protected _handleHistoryChange = ({deleteCount, items}: Mutation<ItemType>) => {
if (deleteCount > items.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you don't know where in the history the items were deleted. This assumes it's at the end. It also doesn't handle replacement.

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<ItemType>) {
routes.forEach(item => {
const route = this._routerMatcher.match(item);
this._appendRoute(route, item.payload);
});
}

private _appendRoute(route: Route, payload?: object) {
if (payload) {
for (const key of Object.keys(payload)) {
if (key in route.page) {
route.page[key] = payload[key];
}
}
}
this._navigationView.append(route.page);
}

private _syncHistoryWithNavigationView() {
while (this.history.length !== this._navigationView.children().length) {
this._routerHistoryObserver.removeLast();
}
}

}
53 changes: 53 additions & 0 deletions src/api/router/RouterHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ListLikeObvserver } from "../../internals/ListLikeObserver";
import { Mutation, List, ListLike } from "../List";
import { Dictionary } from "./Route";

export type HistoryItem = { route: string, payload?: Dictionary<string> };

export class RouterHistory<T extends HistoryItem = HistoryItem> {

private _observer: ListLikeObvserver<T>;

constructor(_callback: (ev: Mutation<T>) => void) {
this._observer = new ListLikeObvserver<T>(_callback);
}

public push(item: T) {
const source = this.getSource();
source.push(item);
this._observer.source = source;
}

public pop(): T {
const source = this.getSource();
const result = source.pop();
this._observer.source = source;
return result;
}

public removeLast() {
return this._observer.source.pop();
}

get history() {
return this._observer.source;
}

set history(value: ListLike<T>) {
this._observer.source = value;
}

get current() {
return this._observer.source[this._observer.source.length - 1];
}

private getSource() {
if (this._observer.source instanceof Array) {
return Array.from(this._observer.source);
} else if (this._observer.source instanceof List) {
return List.from(this._observer.source);
}
throw new Error('Unsupported type');
}

}
18 changes: 18 additions & 0 deletions src/api/router/RouterMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Route } from "./Route";
import { HistoryItem } from './RouterHistory';
import { resolve } from "../..";

export class RouterMatcher {
match(historyItem: HistoryItem): Route {
const name = historyItem.route;
const route = this._createRoute(name);
if (!route) {
throw new Error(`Route with '${name}' name does not exist!`);
}
return route;
}

private _createRoute(name: string): Route {
return resolve(Route, name);
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down