Introduction
Routing is an essential part of every application that displays multiple pages. It allows to develop each part of an application separately and add new parts instantly. As it happens to be in MVC frameworks, each route targets specific controller which takes control over what happens next after a route is matched.
Setting up Router
All routes in IMA.js are registered inside the init
function in app/config/routes.js
. Same init
function can be found in app/config/bind.js
. See Object Container documentation for more information about the oc.get()
function.
Usually you should be oke with simple string defined StaticRoutes (the ones defined below), but the router also has support for more advanced and powerful DynamicRoutes. For more information about these see the next section.
import { RouteNames } from '@ima/core';
import HomeController from 'app/page/home/HomeController';
import HomeView from 'app/page/home/HomeView';
export let init = (ns, oc, config) => {
const router = oc.get('$Router');
router
.add('home', '/', HomeController, HomeView)
.add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
.add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
}
The router add
method has following signature:
add(name, pathExpression, controller, view, options = undefined);
name
string
This argument represents unique route name. You can use this name when linking between routes or getting the route
instance using getRouteHandler()
method.
pathExpression
string | object
This can be either object
for dynamic routes or string
representing route path. The pathExpression supports **parameter substitutions
controller
string | function
Route assigned Controller class (can be a string alias, referring to the controller registered in the Object Container). It goes through its full lifecycle and renders the View.
view
string | function
Route assigned View class (also can be a string alias, referring to the view registered in the Object Container). Rendered by the route controller.
options
object = undefined
These are optional, however it accepts object with following properties and their respective defaults:
{
onlyUpdate: false,
autoScroll: true,
allowSPA: true,
documentView: null,
managedRootView: null,
viewAdapter: null,
middlewares: []
}
onlyUpdate
boolean | function = false
When only the parameters of the current route change an update
method of the active controller will be invoked instead of re-instantiating the controller and view. The update
method receives prevParams
object containing - as the name suggests - previous route parameters.
If you provide function to the onlyUpdate
option; it receives 2 arguments (instances of previous controller and view) and it should return boolean.
autoScroll
boolean = true
Determines whether the page should be scrolled to the top when the navigation occurs.
allowSPA
boolean = true
Can be used to make the route to be always served from the server and never using the SPA (when disabled) even if the server is overloaded.
This is useful for routes that use different document views (specified by the documentView
option), for example for rendering the content of iframes.
documentView
AbstractDocumentView = null
Custom DocumentView, should extend the AbstractDocumentView
from @ima/core
.
managedRootView
function = null
Custom ManagedRootView
component, for more information see rendering process.
viewAdapter
function = null
Custom ViewAdapter
component, for more information see rendering process.
middlewares
function[] = []
Array of route-specific middlewares. See the middlewares section for more information.
Route params substitutions
The parameter name can contain only letters a-zA-Z
, numbers 0-9
, underscores _
and hyphens -
and is preceded by colon :
.
router.add(
'order-detail',
'/user/:userId/orders/:orderId',
OrderController,
OrderView
);
The userId
and orderId
parameters are then accessible in OrderController
via this.params
:
import { AbstractController } from '@ima/core';
class OrderController extends AbstractController {
load() {
const userPromise = this._userService.get(this.params.userId);
const orderPromise = this._orderService.get(this.params.orderId);
return {
user: userPromise,
order: orderPromise
}
}
}
Optional parameters
Parameters can also be marked as optional by placing question mark ?
after the colon :
.
router.add(
'user-detail',
'/profile/:?userId',
UserController,
UserView
);
Optional parameters can be placed only after the last slash. Doing otherwise can cause unexpected behavior.
Linking between routes
URLs to routes can be generated via the Router.link()
public method. These can be then used in ordinary anchor tags and IMA.js makes sure, to handle the site routing in SPA mode, rather than doing redirect/reload of the whole page.
import { AbstractComponent } from '@ima/react-page-renderer';
class OrderView extends AbstractComponent {
render() {
const { user, order } = this.props;
const orderLink = this.link('order-detail', {
userId: user.id,
orderId: order.id
});
return <a href={orderLink}>View order</a>
}
}
This is done by listening to window popstate
and click
events and reacting accordingly (in the listen
method of ClientRouter, which is called by IMA.js on client during app init). If the handled URL is not valid registered app route, it is handled normally (e.g you are redirected to the target URL).
You can use this.link
helper method in IMA.js abstract component or the useLink
hook from the @ima/react-hooks plugin in your components and views to generate router links.
Under the hood, this.link()
is only alias for this.utils.$Router.link
, where this.utils
is taken from this.context.$Utils
.
For more information about this.utils
and $Utils
objects, take a look at the React Context in the documentation.
Generating links outside of app components
Linking in Controllers, Extensions, Helpers and other Object Container classes requires you to import Router
using dependency injection. To do that you can either use Router
class in the dependency array, or $Router
string alias:
import { AbstractController } from '@ima/core';
export default class DetailController extends AbstractController {
static get $dependencies() {
return ['$Router'];
}
constructor(router) {
this._router = router;
}
load() {
// ...
}
}
Then you get Router
instance as the constructor's first argument, which gives you access to it's link
public method (and many others), that you can use to generate your desired route URL:
load() {
const detailLink = this._router.link('order-detail', {
userId: user.id,
orderId: order.id
});
return { detailLink };
}
Error and NotFound route names
There are two special route names that @ima/core
exports: RouteNames.ERROR
, RouteNames.NOT_FOUND
. You can use these constants to provide custom views and controllers for error handling pages.
import { RouteNames } from '@ima/core';
import { ErrorController, ErrorView } from 'app/page/error';
import { NotFoundController, NotFoundView } from 'app/page/not-found';
export let init = (ns, oc, config) => {
const router = oc.get('$Router');
router
.add('home', '/', HomeController, HomeView)
.add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
.add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);
}
Redirects
In addition to the link
method mentioned above (which handles URL generation for given routes), you can use Router.redirect()
method to redirect directly to the targeted URL.
This URL can be either existing app route or external URL. As with links, in this case you also get SPA routing, in case of redirection to different IMA.js app route.
import { AbstractController, Router } from '@ima/core';
export default class DetailController extends AbstractController {
static get $dependencies() {
return [
Router // We're using class descriptor in this case for DI
];
}
constructor(router) {
this._router = router;
}
init() {
this._router.redirect(
this._router.link('order-detail', {
userId: user.id,
orderId: order.id
});
);
}
}
On client side, redirections are handled by simply changing the window.location.href
, while on server you're using the express native res.redirect
method.
Method signature
The redirect method has following signature, while the options object is available only on server side:
redirect(
url = '',
options = {} // Available only on server side
)
url
string
Target redirect URL.
options
object = {}
Additional options, used to customize redirect server response.
{
httpStatus: 302,
headers: undefined,
}
httpStatus
number = 302
Custom redirect http status code.
headers
object = undefined
Custom response headers.