Smartface Router
What is a Router
Router is a concept that decouples application's page-routing logic than view layer in order to make application more
- Manageable
- Maintainable
- Flexible for future growth and change
- Readable
- Useful
Types of Smartface Routers
There are 3 types of routers
ChangeLog
- 2.0.0
- Router module have been fully converted into TypeScript!
- 1.2.0
- Added Replace action to recall current route's lifecycle-methods
- Fix bug #25
Installation
Router already comes preinstalled in the Smartface Workspace.
If you have accidently deleted it or want to install at someplace else, use this command
yarn add @smartface/router
Table of Contents
Getting Started
NativeRouter
is the root routerStackRouter
is to create route stackRoute
is a definition of a pathBottomTabBarRouter
is to manage BottomTabBarController's
Push a new page
import {
NativeRouter: Router,
Router: RouterBase,
NativeStackRouter: StackRouter,
BottomTabBarRouter,
Route
} from "@smartface/router";
const router = Router.of({
path: "/",
to: "/pages/page1",
isRoot: true,
routes: [
Route.of({
path: "/pages/page1",
build: (router, route) => {
let Page2 = require("pages/page2");
return new Page2();
}
}),
Route.of({
path: "/pages/page2",
build: (router, route) => {
const { routeData, view } = route.getState();
let Page1 = require("pages/page1");
return new Page1(routeData, router);
}
})
]})
router.push("/pages/page1");
Go back to a desired page in same history stack
goBack method is functional only if it's used on a StackRouter. And if provided,
related page of the url parameter must be in the same stack history. Otherwise
goBack does nothing.
const router = Router.of({
path: "/",
isRoot: true,
routes: [
StackRouter.of({
path: "/pages",
routes: [
Route.of({
path: "/pages/page1",
build: (router, route) => {
const Page = require("pages/page1");
return new Page({ label: 1 }, router, "/pages2/page2");
}
}),
Route.of({
path: "/pages/page2",
build: (router, route) => {
const Page = require("pages/page1");
return new Page({ label: 2 }, router, "/pages2/page3");
}
}),
Route.of({
path: "/pages/page3",
build: (router, route) => {
const Page = require("pages/page1");
return new Page({ label: 3 }, router, "/pages2/page4");
}
}),
Route.of({
path: "/pages/page4",
build: (router, route) => {
const Page = require("pages/page2");
return new Page({}, router, -2);
}
})
]
})
]
});
router.push("/pages/page1");
router.push("/pages/page2");
router.push("/pages/page3");
router.push("/pages/page4");
import System from '@smartface/native/device/system';
import Application from '@smartface/native/application';
import AlertView from '@smartface/native/ui/alertview';
import { NativeStackRouter } from '@smartface/router';
import Page2Design from 'generated/page2';
export default class Page2 {
router: any;
constructor(data, router) {
super();
this._router = router;
if (this.router instanceof NativeStackRouter) {
this.router.setHeaderBarParams({visible: false});
}
}
}
function btn_onPress() {
this.router.goBacktoUrl("/pages/page1");
this.router.goBacktoHome();
this.router.goBackto(-3);
if (this.router.canGoBack(-2)) {
} else if (this.router.canGoBacktoUrl("/some/path/to/back")) {
} else {
}
}
Replace active route's view using Replace action
Replace action provides rerendering for opened route.
Working with StackRouter
import {
NativeRouter: Router,
Router: RouterBase,
NativeStackRouter: StackRouter,
BottomTabBarRouter,
Route
} from "@smartface/router";
const router = Router.of({
path: "/",
to: "/pages/page1",
isRoot: true,
routes: [
StackRouter.of({
path: "/bottom/stack2",
to: "/bottom/stack2/path1",
headerBarParams: () => ({ ios: { translucent: false } }),
routes: [
Route.of({
path: "/pages/page1",
build: (router, route) => {
let Page2 = require("pages/page2");
return new Page2();
}
}),
Route.of({
path: "/pages/page2",
build: (router, route) => {
const { routeData, view } = route.getState();
let Page1 = require("pages/page1");
return new Page1(routeData, router);
}
})
]
})
]
});
router.push("/pages/page1");
Present & dismiss StackRouter's view as modal
export = StackRouter.of({
path: "/example/modal",
to: "/example/modal/page1",
routes: [
StackRouter.of({
path: "/example/modal/modalpages",
modal: true,
routes: [
Route.of({
path: "/example/modal/modalpages/page1",
build: (router, route) => {
let Page = require("pages/page1");
return new Page({ label: 1 }, router);
}
}),
Route.of({
path: "/example/modal/modalpages/page2",
build: (router, route) => {
let Page = require("pages/page2");
return new Page({ label: 2 }, router);
}
})
]
})
]
});
router.dismiss();
router.dismiss(() => router.push("/to/another/page"));
Working with BottomTabBarRouter
import {
NativeRouter: Router,
Router: RouterBase,
NativeStackRouter: StackRouter,
BottomTabBarRouter,
Route
} from "@smartface/router";
import Color from '@smartface/native/ui/color';
const router = Router.of({
path: "/",
to: "/pages/page1",
isRoot: true,
routes: [
BottomTabBarRouter.of({
path: "/bottom",
to: "/bottom/stack2/path1",
tabbarParams: () => ({
ios: { translucent: false },
itemColor: {
normal: Color.RED,
selected: Color.YELLOW
},
backgroundColor: Color.BLUE
}),
items: () => [{ title: "page1" }, { title: "page2" }, { title: "page3" }],
routes: [
StackRouter.of({
path: "/bottom/stack",
to: "/bottom/stack/path1",
headerBarParams: () => ({ ios: { translucent: false } }),
routes: [
Route.of({
path: "/bottom/stack/path1",
build: (router, route) =>
new Page1(route.getState().routeData, router, "/stack/path2")
}),
Route.of({
path: "/bottom/stack/path2",
build: (router, route) => {
const { routeData, view } = route.getState();
return new Page2(routeData, router, "/bottom/stack2/path1");
}
})
]
}),
StackRouter.of({
path: "/bottom/stack2",
to: "/bottom/stack2/path1",
headerBarParams: () => ({ ios: { translucent: false } }),
routes: [
Route.of({
path: "/bottom/stack2/path1",
build: (router, route) =>
new Page1(
route.getState().routeData,
router,
"/bottom/stack/path2"
)
}),
Route.of({
path: "/bottom/stack2/path2",
build: (router, route) => {
return new Page2(route.getState().routeData, router);
}
})
]
}),
Route.of({
path: "/bottom/page1",
build: (router, route) => {
console.log(`route ${route}`);
return new Page1(
route.getState().routeData,
router,
"/bottom/stack/path1"
);
}
})
]
})
]
});
router.push("/pages/page1");
Working with Pages
const router = Router.of({
path: "/",
routes: [
StackRouter.of({
path: "/bottom/stack2",
to: "/bottom/stack2/path1",
headerBarParams: () => ({ ios: { translucent: false } }),
routes: [
Route.of({
path: "/bottom/stack2/path1",
build: (router, route) =>
new Page1(route.getState().routeData, router)
}),
Route.of({
path: "/bottom/stack2/path2",
build: (router, route) => {
return new Page2(route.getState().routeData, router);
}
})
]
})
]
});
router.push("/bottom/stack2/path1", { sort: "ASC" });
import Page2Design from 'generated/page2';
export default class Page2 {
sort: any;
router: any;
constructor(data, router) {
super();
this.sort = routeData.sort;
this.router = router;
}
}
function btnNext_onPress() {
const page = this;
page.router.push("/path/to/another", {
message: "Hello World!"
});
}
Setting home-route to StackRouter
homeRoute property of StackRouter is the index of the first route in the stack.
StackRouter.of({
path: "/bottom/stack2",
to: "/bottom/stack2/path1",
homeRoute: 0,
headerBarParams: () => ({ ios: { translucent: false } }),
routes: [
Route.of({
path: "/bottom/stack2/path1",
build: (router, route) =>
new Page1(route.getState().routeData, router, "/bottom/stack/path2")
}),
Route.of({
path: "/bottom/stack2/path2",
build: (router, route) => {
return new Page2(route.getState().routeData, router);
}
})
]
});
Send and recevice query-string
const route = Router.of({
StackRouter.of({
path: "/bottom/stack2",
to: "/bottom/stack2/path1",
homeRoute: 0,
headerBarParams: () => { ios: { translucent: false } },
routes: [
Route.of({
path: "/bottom/stack2/path1",
build: (router, route) => {
const sort = route.getState().query.sort;
return new Page1(route.getState().routeData, router, sort, "/bottom/stack/path2")
}
}),
Route.of({
path: "/bottom/stack2/path2",
build: (router, route) => {
return new Page2(route.getState().routeData, router);
}
})
]
})
});
router.push('/bottom/stack2/path1?sort=ASC&groupby=user')
Working with deeplinking
import {
Route,
Router: RouterBase,
NativeRouter: Router
} from "@smartface/router";
const deeplinkRouter = new RouterBase({
path: "/deeplink",
routes: [
Route.of({
path: "/deeplink/products/:id",
to: (router, route) => {
const state = route.getState();
return "/nav/tabs/discover/products" + state.match.params.id + '/' + state.rawQuery;
}
}),
Route.of({
path: "/deeplink/product/:id",
to: (router, route) => {
return "/nav/product/display/" + route.getState().match.params.id;
}
}),
Route.of({
path: "/deeplink/search",
to: "/nav/tabs/shop/searchResults"
})
]
});
const router = Router.of({
routes: [deeplinkRouter, ...]
});
Application.onReceivedNotification = function(e) {
const receivedUrl = e.remote.url;
const domain = 'http://yourdomain.com';
const url = receivedUrl.replace(domain, '/deeplink');
deeplinkRouter.push(receivedUrl);
router.push(receivedUrl);
};
Working with life-cycle methods
Routes have some life-cycle events :
routeDidEnter
is triggered when route is activated by exact match to the requested urlrouteDidExit
triggered when route is deactivated by exact match to an another routebuild
is a builder function to create view instance associated with specified router and route or notrouteShouldMatch
is triggered when route is matched as exact and then route will be blocked or not by regarding the return value of the method
Route.of({
path: "/bottom/page1",
routeDidEnter: (router, route) => {
const { view } = route.getState();
view?.onRouteEnter && view.onRouteEnter(router, route);
},
routeDidExit: (router, route) => {
const { view } = route.getState();
view.onRouteExit && view.onRouteExit(router, route);
},
routeShouldMatch: (route, nextState) => {
if (!nextState.routeData.applied) {
return false;
}
return false;
},
build: (router, route) => {
const { view } = route.getState();
return (
view ||
new Page1(route.getState().routeData, router, "/bottom/stack/path1")
);
}
});
Blocking Routes
const unload = router.addRouteBlocker((path, routeData, action, ok) => {
alert({
message: "Would you like to answer?",
title: "Question",
buttons: [
{
text: "Yes",
type: AlertView.Android.ButtonType.POSITIVE,
onClick: () => {
ok(true);
}
},
{
text: "No",
type: AlertView.Android.ButtonType.NEGATIVE,
onClick: () => {
ok(false);
}
}
]
});
});
unload();
Limitations of blockers
Following cases cannot be handled by the blocker:
- iOS HeaderBar: Back gesture of the page & back button action of HeaderBar cannot be prevented. If user wants to use blockers in these cases, custom back button must be used.
- BottomTabBar: Switching between tabs cannot be prevented
Listening history changes
const unlisten = router.listen((location, action) => {
console.log(`new route action : ${action} path : ${location.url}`);
});
unlisten();
Common features of Routers
- Nested routes
- Route redirection
- Route blocking
- History listening
Contribute to Repository
- Clone the repository
- Install dependencies by using
yarn
command - Compile the project by
yarn run build
or invoke it in watch mode by yarn run watch
- Create a symlink in your Smartface Worksace path
scripts/node_modules/@smartface/router
.
Example command(don't forget to delete the installed lib directory): ln -s /path/to/your/smartface/workspace/scripts/node_modules@smartface/router/lib /path/to/your/router/installation/lib
You can also use NPM Workspaces feature to link two directories. However, this method will cause your node_modules
files to merge and the development environment will increase in size dramatically.
Test Driven Development(TDD)
Tests will run on before every publish and all of the test cases are required to pass before a new version can be published.
yarn test -- --watch
Update Documentation
Documentation will be updated every time when changes are pushed into the master
branch.
To do it manually or oversee the documentation beforehand:
yarn run docs
open ./docs/index.html // Mac Only