This page covers use of CodeOnly's Router for more complex setups than those covered by basic URL routing.
When working with the router, there's a few terms and concepts you should be familiar with. We'll cover all these in detail, but it's good to have a high level view of all the pieces:
Router - the central controller of route operations. The router receives URL load requests, matches them to route handlers and fires events that provide hooks into the navigation process.
Navigation Events - on each navigation the router goes through a sequence of steps to fully resolve and handle the URL. As it does so it calls notification methods on the source and target route handlers and fires events to other attached listeners.
Route Handlers - these are objects your application registers with the router that are responsible for matching recognized URLs and populating the route object with information about the page to be shown.
Route Objects - a route object stores everything about the current navigation. This includes the URL, the matched handler and anything else the handler (ie: your application) wants to associate with this route.
Router Driver - the router itself interacts with the browser through a
"driver". The included WebHistoryRouterDriver
provides the glue
between the router and the browser's History API.
View State Restoration - when navigating through the browser history there is usually transient "view state" such as the current scroll position that needs to be captured and restored.
Internal URLs - URLs in a form understood by your application.
External URLs - URLs in a form as seen by the browser (and the user).
URL Mapper - an object that internalizes and externalizes URLs.
Let's take a look at each of these concepts in a more detail...
The Router
class is the main participant in routing operations. Its
responsibilities include:
There is always only ever a single instance of the router, available
as router
import { router } from "@codeonlyjs/core";
The router also has methods that can be used by your app to
invoke navigation (navigate
, back
, replace
) but these simply
pass through to the router driver.
When the router receives a URL load request and after it has matched it to a route handler, it proceeds through a series of steps to "leave" the old route, and "enter" the new route.
These steps take place in two phases:
The "may navigate" phase - a series of events where route handlers and event listeners can perform async operations and optionally cancel the navigation.
The "will navigate" phase - a synchronous set of non-cancellable steps that notify that the old route will be left and the new route will be entered.
Within each phase, the Router fires two kinds of events:
The full sequence of events is as follows:
Firstly, Phase 1, the cancellable, async "may navigate" phase starts:
mayLeave
" eventmayLeave
on the from
route handlermayEnter
on the to
route handlermayEnter
" eventIf this point is reached without the navigation being cancelled, then Phase 2, the synchronous, non-cancellable "will navigate" phase starts:
didLeave
" eventdidLeave
on the from
route handlerdidEnter
on the to
route handlerdidEnter
" eventNote that in phase 1, the navigation might be cancelled by the event handlers, the route handlers or by a second URL load request being received before the first one has finished.
A second load request might come from the user navigating the browser history or it might come from your app initiating a navigation before the first one has completed.
A route handler is an object that handles the navigation to and from a particular URL.
Route handlers are registered with the router during app startup and are called by the router when a URL is loaded and needs to be matched to a particular handler.
When a route handler matches a URL it will usually store additional information on the route object that describes the component or page to be displayed for that URL along with any other information the handler or the application might find useful.
Route handlers can have the following members, none of which are required:
{
pattern: "/url/pattern",
match: async (to) => true;
mayLeave: async (from, to) => true;
mayEnter: async (from, to) => true;
didLeave: (from, to) {},
didEnter: (from, to) {},
cancelLeave: (from, to) {},
cancelEnter: (from, to) {},
order: 0,
}
The pattern
parameter can be either a URL pattern string, or a regular
expression object. If not specified, all URLs are matched.
When the URL matches a handler's pattern, the handler's match
function is
called to confirm the match. It should return true to accept the URL
or false to reject it.
If the match
handler returns null
, this instructs the router and router
driver to ignore the navigation event and revert to a normal browser
page load. This can be used to redirect routes to external pages.
If present, the match
, mayLeave
and mayEnter
functions must all
return true for navigation to succeed. These methods can all return a
promise for the true
/false
result.
The from
argument is the route object being navigated away from (leaving).
On initial page load from
will be null
.
The to
argument is the route object being navigated to (entering).
The cancelEnter
and cancelLeave
functions are called if a navigation
was cancelled after a previous notification of mayEnter
or mayLeave
respectively.
The order
parameter controls the order in which route handlers are
matched to URLs. It can be used to resolve conflicts where two handlers would
otherwise both match a URL. It is often used to configure a "Not Found"
route handler to match any URL - after all other handlers failed to match.
A route object stores everything related to a particular URL load operation.
Route objects are never re-used and only last from the start of the URL load that created it, until the URL is navigated away from.
ie: navigating "back" doesn't re-use the same route object instance as last time - a new object is created every time.
Route objects are initially created by the Router with the following properties:
{
url: new URL(),
state: { }
current: false,
}
Once a handler has been matched to a URL a reference to the handler will be added to route object:
{
url: new URL(),
state: { },
current: false,
handler: { }
}
These are the known, built-in properties of the route:
url
- the internalized URL of the route (this is a URL object, not a string)state
- any previously saved state associated with the URLcurrent
- true if this is the route that would be returned by router.current
handler
- the matched handler for this routeAt any point event and route handlers can add additional information to the route object.
eg:
Route handlers will typically create a page component and store it on the route object to be picked up elsewhere in the app to be loaded into the document.
Other event handlers may also attach additional information to the route object.
eg: the built-in ViewStatePersistence
object, in its mayEnter
event handler,
stores any previously saved view state as viewState
property on the route object.
The router driver connects the router to its hosting environment.
Most typically this is the browser. The built-in WebHistoryRouterDriver
class
works with the browser's History API to handle all browser navigation.
The WebHistoryRouterDriver
also listens for clicks on anchor <a>
elements,
inspects the href
attribute and if it looks like an in-page link, instructs
the router to initiate a URL load for the href.
The router driver is selected by creating an instance and passing it to
the router's start
method. If no driver is specified, an instance
of the WebHistoryRouterDriver
is used.
Currently the WebHistoryRouteDriver
is the only available driver, but may be
expanded with a similar driver for the Navigation API. A server side
router driver will also be developed for use in server-side rendering scenarios.
View state restoration is the process of maintaining any otherwise transient view state information when navigating through the session history.
The most common kind of view state to be persisted is the current scroll position which is handled automatically.
View state restoration can be customized:
on a per-route basis by adding captureViewState()
and restoreViewState(viewState)
functions to the route handler.
globally by adding captureViewState()
and restoreViewState(viewState)
methods to the router object.
The router supports a concept of internal and external URLs:
Whenever the router driver receives a URL from the browser it first calls
the Router's internalize
method to convert it to an internal URL. Similarly
before passing any internal URL to the browser it first calls the Router's
externalize
method to convert the internal URL to an external one.
The internalize
and externalize
methods can accept URL objects (in which
case a new URL object is returned), or string URLs (in which case a string
is returned).
The default internalize
and externalize
methods just return a copy of the
the same URL.
If you set the router's urlMapper
property to anyobject with internalize
and externalize
methods the Router will forward these calls to that object.
CodeOnly includes a UrlMapper
object for a couple of common use cases...
The UrlMapper
class provides support for internalizing and externalizing
URLs with a base prefixes and/or hashed URL paths instead of normal URL paths.
Base prefixes can be used to load a single page app into a sub-path of the root domain.
For example, suppose you wanted your single page app to appear in a sub-path
/myapp
:
router.urlMapper = new UrlMapper({
base: "/myapp/"
});
Now any URL's received from the browser will have the /myapp
prefix removed
and URL's generated by your app will have the /myapp
prefix prepended.
eg: an external URL of /myapp/about
would appear to your application
(and the router) as /about
.
The UrlMapper
can also be used for hashed URL paths. This can be used when
you have a single page application that doesn't have the required server-side
support for normal URL paths:
router.urlMapper = new UrlMapper({
hash: true,
});
When hashed URLs are enabled, an internal URL of /about
would be
externalized to /#/about
(and vice-versa)
Note: when creating <a>
links in your application, you should use the external
URL not the internal one. This can be done by calling the router's
externalize()
method:
{
type: "a",
href: router.externalize("/about"),
text: "About this Site",
}
In the above example, we're not using a dynamic callback
for the href
attribute because it's not a dynamic
value.