Skip to content

Latest commit

 

History

History
296 lines (206 loc) · 8.95 KB

README.md

File metadata and controls

296 lines (206 loc) · 8.95 KB

🐉 Dragon Router

Dragon Router is an ExpressJS-like client side router built from the ground up on debuggability and simplicity.

It uses the browser's history API to control the pushing and popping of page navigation; overwriting the need for a full page refresh on user navigation. Add it to your project with NPM:

npm install --save dragon-router

Try the demo:

Edit dragon-router-demo

Setup and usage

ES6

Router is an es6 class. Import it like you would any other module. After setting up your routes (See below), register the router on the window.

import { Router } from 'dragon-router';
const router = new Router();

... // add routing rules here

router.registerOn(window);
router.start() // run this function after you have established routing rules, so that the router knows it should immediately apply them

CommonJS

If you are using CommonJS, you may import the proper version from the /dist folder.

const { Router } = require('dragon-router/dist/dragon-router.min.js')

Native Browser Sourcing

Likewise, you can include it in your HTML from a script tag.

<!-- Globally Registered -->
<script src="/path/to/dragon-router/dist/dragon-router.min.js"></script>

OR

<!-- ES6 Module Imports -->
<script type="module" src="/path/to/dragon-router/dist/dragon-router.module.js"></script>

Options

let options = {
  basePath: '/my-app/base/route',    // mount the router off of a specific path     [default is '/']
  routerId: 'my-cool-dragon-router', // unique identifier between apps              [default is a random number]
  registerOn: window,                // bind to the client's browser immediately    [if not given, `router.registerOn(...)` must be called separately]
  debug: true                        // show additional logging info                [default is false]
}

const router = new Router(options);

Router.use()

The .use() method allows us to apply matchers or behaviors to the routing.

Route matching

Route matching follows a similar pattern as Express. You can match with literal paths or parameterized paths (which populate the Context with parameters).

// render a page on a literal path matching
router.use('/about', renderYourAboutPageCB);
router.use('/:section/:subSection', (context) => {
  
  // prefixing a path section with ':' will name that section in `context.params` 
  let section = context.params.section;
  let subSection = context.params.subSection;

  // now you can use the grepped data to apply on your app.
  renderYourPageCB(section, subSection);

});

You can append a parameter declaration with ( ) to specify a regex pattern to enforce a match.

router.use('/:section(home|about)/:subSection', (context) => {

  // now, the path will only ever match if the `section` is 'home' or 'about'
  let section = context.params.section;
  
  ...
});

Additionally, you may apply an array of matchers to a given handler.

router.use(['/home/:subSection', '/about/:subSection'], (context) => {
  // your code here
});

Full RegExp matchers are also supported. (Note that these do not get parameterized, unlike the string matchers mentioned above)

router.use(/^\/some\/fancy\/regular\/expression$/, (context) => {
  // your code here
});

Optional Subpaths

Additional syntax of matchers includes * and ? postfixes to sections.

The * postfix (e.g. /your/route*) will match any incoming route that is prefixed with the text before the * character.

The ? postfix (e.g. /your/:route?) allows that section of the route to be optional. If you want to have the router automatically populate an optional section with data, see Derived Subpaths below.

Derived Subpaths

A DerivedSubpath allows for a route to specify default values for an optional path. These are derived from a given callback. The callback for the DerivedSubpath can return an async object or a String. This is especially useful for automatically redirecting to fully qualified paths in your app.

Here is an example:

let defaultSection = new DerivedSubpath('section', (context) => {
  return 'main'; // or whatever you need to do to compute the default `section`
})
router.use(defaultSection);

...

// prefixing a parameter with '$' tells the router we want to use a DerivedSubpath
router.use('/page/$:section(main|about|contact)', renderPageSectionCB)

In this example, the section parameter will always be defined when renderPageSectionCB is ran. If the user goes to /page, they will be redirected to /page/main by the router.

By this principle, the following two blocks are functionally equivalent:

router.use('/page', (context) => {
  router.redirect(`/page/main`);
})
router.use('/page/:section(main|about|contact)', (context) => {
  router.redirect(`/page/${context.params.section}/default`);
});
router.use('/page/:section(main|about|contact)/:subsection(default|other)', renderPageCB);

and

router.use(new DerivedSubpath('section', () => 'main'));
router.use(new DerivedSubpath('subsection', () => 'default'));
router.use('/page/$:section(main|about|contact)/$:subsection(default|other)', renderPageCB);

The latter being easier for the developer to hold in their mental model of the routing.

Middleware

Middleware is a pipeline of functions that get applied when a matching route is rendered. A middleware function takes two parameters, context, and next. The next argument is a callback that invokes the next middleware in the pipeline. Naturally, next() should not be called in the last function of the pipeline.

let loggingMiddleware = (context, next) => {
  console.log('[Router]: navigating to ', context.path)
  next(); 
}

...

router.use('/:view', loggingMiddleware, renderYourViewCB);

Alternatively, functions that are given to Router.use(...) without a matcher are treated as global middleware, and ran for every route.

router.use(loggingMiddleware);
router.use(someOtherMiddleware1, someOtherMiddleware2); // accepts multiple middleware in one `use` statement

RouteHandler

If you wish to make a particular handler reusable, you may form it as a RouteHandler for your convenience.

The constructor of RouteHandler takes two arguments:

let handler = new RouteHandler('/demo', [middleware1, middleware2, (context) => {
  // your handle here
}]);

router.use(handler);

Removing the Router

In the event that you wish to remove the router from your application, you will need to first unregister it before deleting.

router.unregister()
router = undefined; // or whatever your flavor of deleting objects in JS

Debugging

Dragon Router was built with debuggability in mind. In the console, you have access to valuable information that can help you understand what the router is doing.

> window.attachedRouter

This feature allows us to poke into the internals of the router and understand information about the callback registrar and what paths are handled by which callbacks.

Additionally, when you add the option { debug: true } to the router, we get helpful output from the router live as the user navigates around the page.

const router = new Router({ debug: true });

Using with ReactJS

With Hooks, integration into ReactJS is pretty simple. Below is an example on how to do so.

// Import our router and the pages we want to client render
import React from 'react'
import { Router } from 'dragon-router'
import { DefaultPage, ExamplePage } from '../my-client-rendered-pages'

// This function sets up the router to select a page to render based off of the current path
function attachRouter (updatePage) {
  return () => {
    const router = new Router({ registerOn: window })
    
    // set up routing rules
    router.use('/example', ctx => updatePage(<ExamplePage context={ctx} />))
    router.use('*', ctx => updatePage(<DefaultPage context={ctx} />))
    router.start()

    // cleanup callback
    return () => router.unregister()
  }
}

// Now our app conditionally renders pages based off of the route we are on
export default function App ({}) {
  const [page, updatePage] = React.useState(<DefaultPage />)
  React.useEffect(attachRouter(updatePage), [])

  return <div>{page}</div>
}