marko-path-router v0.7.3
marko-path-router
Client side routing for Marko that provides support for wildcard, placeholder, and nested routes. A small demo app can be found here.
Installation
npm install --save marko-path-router
# or if you use yarn
yarn add marko-path-routerUsage
Creating the 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.
Navigation
To navigate to a route, you can use the provided router-link component.
router-link path='/users'
  -- Go to /usersUpon 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.
Nested Routes
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.
Placeholder routes
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/1or/users/8bdc5071-7de1-4282-af12-f6f0a9c431f1will render theusercomponent.
- Navigating to /users,/users/, or/users/3/descriptionwill miss and cause the router to emit anot-foundevent.
- Navigating to /group, will render thegroup-listcomponent and navigating to/group/26or/group/8bdc5071-7de1-4282-af12-f6f0a9c431f1will render thegroupcomponent 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
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 /userwill render theusercomponent.
- Navigating to /user/infowill render theusercomponent with theuser-infocomponent rendered as a child.
- Navigating to /user/2, or/user/some/path/that/does-not/existwill render theusercomponent with theuser-catch-allcomponent rendered as a child.
- Any other route will be caught by the wildcard route and will render the catch-allcomponent.
Hash history
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 /#/usersPassing data to route components
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 hooks
Global beforeEach and afterEach hooks can be registered to allow for a little more
control over route transitions.
The beforeEach hook
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.
The afterEach hook
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}!`)
})