Client side routing for Marko that provides support for wildcard, placeholder, and nested routes. A small demo app can be found here.
npm install --save marko-path-router
# or if you use yarn
yarn add marko-path-router
Creating a router is simple. First, you need to define the routes that you want the router to handle.
Each route must have a path
and a component
.
const routes = [
{ path: '/', component: require('src/components/home') },
{ path: '/users', component: require('src/components/users') },
{ path: '/directory', component: require('src/components/directory') }
]
Note: At the moment, routing only works with renderers that are linked to a component definition
(either via class {...}
in a single file component or via an object/class exported in a component.js
file).
This is needed because instances of components are tracked to ensure that only the required parts of the view are
updated.
Next, pass in the routes
and the initialRoute
that should be rendered to the Router
component.
const { Router } = require('marko-path-router')
const render = Router.renderSync({
routes: routes,
initialRoute: '/'
})
const routerComponent = render.appendTo(targetEl).getComponent()
Alternatively, you can pass the data to the router
tag.
div.my-app
div.app-header
div.header-title -- marko-path-router
div.app-content no-update
router routes=state.routes initialRoute='/'
In the example above, the home
component will be rendered within the router
component.
Note: It is recommended that the router is placed within a element that is marked with no-update
.
This will ensure that the router will not get rerendered by it's parent and will prevent
the rendered view from being lost because of actions happening outside of the router.
To navigate to a route, you can use the provided router-link
component.
router-link path='/users'
-- Go to /users
Upon clicking the router-link
, the router will perform a lookup. If the router has found a match
, it will render the component mapped to the path and emit an update
event. If a router does not find a match
it will emit a not-found
event.
You can add a listener to the router component and handle events accordingly.
routerComponent.on('update', () => {
// handle router update
})
routerComponent.on('not-found', () => {
// handle not found
})
Note: If you are rendering the router via the router
tag, you can add a key
attribute to it and
retrieve the component via this.getComponent(routerKey)
.
router routes=state.routes initialRoute='/' key='my-router'
If needed, you can use the module's history
wrapper for pushing or replacing state as an alternative
to the router-link
.
const { history } = require('marko-path-router')
// push browser history
history.push('/users')
// replace current route
history.replace('/directory')
The history
object also exposes the native history's back
, forward
, and go
methods for convenience.
You can nest routes by providing the optional nestedRoutes
attribute for a route. The path
given to
nested route is appended to it's parent's path when the routing tree is built. This can be added to any route, so you
have as many layers as you desire.
const routes = [
{
path: '/charts',
component: require('src/components/charts'),
nestedRoutes: [
{ path: '/line', component: require('src/components/line-chart') },
{ path: '/bar', component: require('src/components/bar-chart') }
]
},
{ path: '/users', component: require('src/components/users') },
]
This configuration will create the following routes:
/charts
/charts/line-chart
/charts/bar-chart
/users
In the example above, navigating to /charts
will only render the charts
component.
Navigating to /charts/line
, will render the charts
component and will also pass the component a renderBody
function that can be used by the charts
component to render the line-chart
component. The renderBody
can be passed
into an include
tag for rendering, much like with regular component nesting in Marko. (More info on the include
tag
here).
Below is an example of how the charts
component can allow for the line-chart
component to rendered.
src/components/charts/index.marko:
div.charts-showcase
div.chart-1 key='chart-1'
div.chart-2 key='chart-2'
div.main-chart
if(input.renderBody)
include(input.renderBody)
The router keeps track of the components that it currently has rendered. So, if it finds that there are existing components that can be reused, it will not perform a render of the entire view.
For example, if we navigate to /charts/line
, the router will render the charts
component and the line-chart
component.
If we then navigate to /charts/bar
, the router will simply update the existing charts
component
with a new renderBody
that will render the bar-chart
component. So there is no unnecessary rerendering and remounting of
components.
Placeholders can be placed into a route by starting a segment of the route with a colon :
.
For example, let's define the following routes:
const routes = [
{ path: '/users/:userId', component: require('src/components/user') },
{
path: '/groups',
component: require('src/components/group-list'),
nestedRoutes: [
{ path: '/:groupId', component: require('src/component/group') }
]
},
]
With a router using the above routes:
- Navigating to
/users/1
or/users/8bdc5071-7de1-4282-af12-f6f0a9c431f1
will render theuser
component. - Navigating to
/users
,/users/
, or/users/3/description
will miss and cause the router to emit anot-found
event. - Navigating to
/group
, will render thegroup-list
component and navigating to/group/26
or/group/8bdc5071-7de1-4282-af12-f6f0a9c431f1
will render thegroup
component as a child ofgroup-list
.
Note: The names that are given to placeholder routes do not matter (you should still give them good names though).
The placeholder values will be added as part of the input
to the router component under the params
attribute.
The params
are provided as an array with its contents sorted by the order the placeholders are defined on
the route's path.
For example, with a route defined as /orgs/:organization/groups/:groupId
, navigating to
/orgs/test-organization/groups/test-group
will render a component with input.params
defined as
[ 'test-organization', 'test-group' ]
Wildcard routes can be configured by adding a /**
to the end of a route. These will act as a catch-all.
const routes = [
{
path: '/user',
component: require('src/components/user'),
nestedRoutes: [
{ path: '/info', component: require('src/component/user-info') }
{ path: '/**', component: require('src/component/user-catch-all') }
]
},
// catch everything else
{ path: '/**', component: require('src/component/catch-all') }
]
With the above configuration:
- Navigating to
/user
will render theuser
component. - Navigating to
/user/info
will render theuser
component with theuser-info
component rendered as a child. - Navigating to
/user/2
, or/user/some/path/that/does-not/exist
will render theuser
component with theuser-catch-all
component rendered as a child. - Any other route will be caught by the wildcard route and will render the
catch-all
component.
By default, the router will use browser history with regular paths, meaning route changes will show up like regular paths in the url. This means that some sort of proxy or catch-all needs to be placed in front of your webapp to get the same bundle/html served.
For users that would rather just have their app served up on a cdn without having to set up proxying,
the router can be set to use hash
history instead.
This can be done by simplying specifying the router's mode
to hash
upon creation.
Example:
const { Router } = require('marko-path-router')
const render = Router.renderSync({
mode: 'hash',
routes: [
{
path: '/user',
component: require('../components/user'),
}
],
initialRoute: '/'
})
const routerComponent = render.appendTo(targetEl).getComponent()
Besides explicitly specifying the router's mode
, nothing else about the router's usage
changes. The router will automatically configure it's internal history
module to
listen to use hash history and listen for hashChange
events.
router-link
usage stays the same, with the slight difference that the href
is prefixed with a #
to more accurately portray the correct route.
router-link path='/users'
-- Goes to /#/users
Components that are associated with routes can be given input values via the injectedInput
attribute.
This allows data stores, app instances, and other common data to be passed down to route components
from the root of your application. If you find yourself needing to communicate between components rendered
via the router, it may be helpful to pass in a common object that can act as an event bus.
const render = Router.renderSync({
initialRoute: '/',
injectedInput: {
app: myApp,
store: myDataStore,
foo: 'bar'
},
routes: [
{
path: '/user',
component: require('src/components/user'),
nestedRoutes: [
{ path: '/info', component: require('src/component/user-info') }
{ path: '/**', component: require('src/component/user-catch-all') }
]
},
]
})
In the above example, the values specified in injectedInput
will be passed down to the user
, user-info
, and user-catch-all
component when they are rendered.
Ex. src/component/user-info/component.js
module.exports = {
onCreate (input) {
// input contains the same values as what was passed as "injectedInput"
// in the above router
const { app, store, foo } = input
this._dataStore = store
this.state = { app }
console.log(foo)
}
}
Global beforeEach
and afterEach
hooks can be registered to allow for a little more
control over route transitions.
For the beforeEach
hook, the from
(current route), to
(next route) and next
function
are passed in to the callback.
If the next
function is invoked without any arguments, the transition will continue to
execute and the next route will be rendered.
If an error
is passed into the next
function, the router will halt the transition
and emit and error
event.
If false
is passed into the next
function, the router will just halt the transition.
For the afterEach
hook, only the from
(current route), to
(next route) are
passed into the callback.
Ex.
const { Router } = require('marko-path-router')
const render = Router.renderSync({
routes: routes,
initialRoute: '/'
})
const router = render.appendTo(targetEl).getComponent()
router.beforeEach((from, to, next) => {
console.log(`Starting transition from ${from} to ${to}!`)
next()
})
router.afterEach((from, to) => {
console.log(`Completed transition from ${from} to ${to}!`)
})