prouter

10.0.26 • Public • Published

prouter

license tests coverage status npm version

Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of express middlewares.

Essentially, give prouter a list of path expressions (routes) and a callback function (handler) for each one, and prouter will automatically invoke these callbacks according to the active path in the URL.

Why prouter?

  • Performance: fast and tiny size (currently under 5kb before gzipping) are both must-haves to smoothly run in any mobile or desktop browser.
  • KISS principle everywhere: do only one thing and do it well, routing! Guards? conditional execution? generic pre and post middlewares? all that and more is easily achievable with prouter (see examples below).
  • Learn once: express router is very powerful, flexible, and simple, why not bring a similar API to the frontend? Under the hood, prouter uses the same (wonderful) library that express for parsing routes path-to-regexp (so it allows the same flexibility to declare routes). Read more about the concept of middlewares here.
  • Unobtrusive: it is designed from the beginning to play well with vanilla JavaScript or with any other library or framework.
  • Forward-thinking: written in TypeScript for the future and transpiled to es5 with UMD format for the present... thus it transparently supports any module style: es6, commonJS, AMD. By default, prouter uses the modern history API for routing.
  • Unit tests for every feature are created.

Installation

# With NPM
npm install prouter --save

# Or with Yarn
yarn prouter --save

# Or just include it using a 'script' tag in your HTML file
<script src="https://cdn.jsdelivr.net/npm/prouter/prouter.min.js"></script>

Examples

basic

// Using es6 modules
import { browserRouter } from 'prouter';

// Instantiate the router
const router = browserRouter();

// Declare the paths and its respective handlers
router
  .use('/', async (req, resp) => {
    const people = await personService.find();
    const html = PersonListCmp(people);
    document.querySelector('.router-outlet') = html;
    // end the request-response cycle
    resp.end();
  })
  .use('/about', (req, resp) => {
    document.querySelector('.router-outlet') =
      `<h1>Some static content for the About page.</h1>`;
    // end the request-response cycle
    resp.end();
  });

// start listening for navigation events
router.listen();

guard middleware which conditionally avoid executing next handlers and prevent changing the path in the URL

// Using commonJs modules
const prouter = require('prouter');

// Instantiate the router
const router = prouter.browserRouter({
  processHashChange: true // this allows to process 'hash' changes in the URL.
});

// Declare the paths and its respective handlers
router
  .use('*', (req, resp, next) => {
    // this handler will run for any routing event, before any other handlers

    const isAllowed = authService.validateHasAccessToUrl(req.path);

    if (!isAllowed) {
      showAlert("You haven't rights to access the page: " + destPath);
      // end the request-response cycle, avoid executing other handlers
      // and prevent changing the path in the URL.
      resp.preventNavigation = true;
      resp.end();
      return;
    }

    // pass control to the next handler
    next();
  })
  .use('/', (req, resp) => {
    // do some stuff...
    // and end the request-response cycle
    resp.end();
  })
  .use('/admin', (req, resp) => {
    // do some stuff...
    // and end the request-response cycle
    resp.end();
  });

// start listening for navigation events
router.listen();

// programmatically try to navigate to any route in your router
router.push('/admin');

run a generic middleware (for doing some generic stuff) after running specific handlers

import { browserRouter } from 'prouter';

// Instantiate the router
const router = browserRouter();

// Declare the paths and its respective handlers
router
  .use('/', async (req, resp, next) => {
    const people = await personService.find();
    const html = PersonListCmp(people);
    document.querySelector('.router-outlet') = html;
    // pass control to the next handler
    next();
  })
  .use('*', (req, resp) => {
    // do some (generic) stuff...
    // and end the request-response cycle
    resp.end();
  });

// start listening for navigation events
router.listen();

modularize your routing code in different files using Router Group

import { browserRouter, routerGroup } from 'prouter';

// this can be in a different file for modularization of the routes,
// and then import it in your main routes file and mount it.
const productRouterGroup = routerGroup();

productRouterGroup
  .use('/', (req, resp) => {
    // do some stuff...
    // and end the request-response cycle
    resp.end();
  })
  .use('/create', (req, resp) => {
    // do some stuff...
    // and end the request-response cycle
    resp.end();
  })
  .use('/:id(\\d+)', (req, resp) => {
    const id = req.params.id;
    // do some stuff with the 'id'...
    // and end the request-response cycle
    resp.end();
  });

// Instantiate the router
const router = browserRouter();

// Declare the paths and its respective handlers
router
  .use('*', (req, resp, next) => {
    // this handler will run for any routing event, before any other handlers
    console.log('request info', req);
    // pass control to the next handler
    next();
  })
  .use('/', (req, resp) => {
    // do some stuff...
    // and end the request-response cycle
    resp.end();
  })
  // mount the product's group of handlers using this base path
  .use('/product', productRouterGroup);

// start listening for the routing
router.listen();

// programmatically navigate to the detail of the product with this ID
router.push('/product/123');

full example: modularized routing, generic pre handler acting as a guard, generic post handler

import { browserRouter, routerGroup } from 'prouter';

// this can be in a different file for modularization of the routes,
// and then import it in your main routes file and mount it.
const productRouterGroup = routerGroup();

productRouterGroup
  .use('/', (req, resp, next) => {
    // do some stuff...
    // and pass control to the next handler
    next();
  })
  .use('/create', (req, resp, next) => {
    // do some stuff...
    // and pass control to the next handler
    next();
  })
  .use('/:id(\\d+)', (req, resp, next) => {
    const id = req.params.id;
    // do some stuff with the 'id'...
    // and pass control to the next handler
    next();
  });

// Instantiate the router
const router = browserRouter();

// Declare the paths and its respective handlers
router
  .use('*', (req, resp, next) => {

    // this handler will run for any routing event, before any other handlers

    const isAllowed = authService.validateHasAccessToUrl(req.path);

    if (!isAllowed) {
      showAlert("You haven't rights to access the page: " + destPath);
      // end the request-response cycle, avoid executing next handlers
      // and prevent changing the path in the URL.
      resp.preventNavigation = true;
      resp.end();
      return;
    }

    // pass control to the next handler
    next();
  })
  .use('/', (req, resp, next) => {

    const doInfiniteScroll = () => {
      // do infinite scroll ...
    };

    const onNavigation = (navigationEvt) => {
      console.log('new path', navigationEvt.oldPath);
      console.log('old path', navigationEvt.newPath);
      // if navigating, then remove the listener for the window.scroll.
      router.off('navigation', onNavigation);
      window.removeEventListener('scroll', doInfiniteScroll);
    };

    window.addEventListener('scroll', doInfiniteScroll);

    // subscribe to the navigation event
    router.on('navigation', onNavigation);

    // and pass control to the next handler
    next();
  })
  .use('/login', () => {
    openLoginModal();
    // as this route opens a modal, we would want to prevent navigation in this handler,
    // so end the request-response cycle, avoid executing next handlers
    // and prevent changing the path in the URL.
    resp.preventNavigation = true;
    resp.end();
  })
  .use('/admin', (req, resp, next) => {
    // do some stuff...
    // and pass control to the next handler
    next();
  })
  // mount the product's group of handlers using this base path
  .use('/product', productRouterGroup)
  .use('*', (req, res, next) => {

    // this handler will run for any routing event, after the other handlers

    // req.listening will be true when this callback was called due to a
    // client-side navigation (useful to differentiate client-side vs
    // server-side rendering - when using a mix of both SSR and CSR)
    if (req.listening) {
      const title = inferTitleFromPath(req.path, APP_TITLE);
      updatePageTitle(title);
    }

    // end the request-response cycle
    resp.end();
  });

// start listening for the routing
router.listen();


// the below code is an example about how you could capture clicks on links,
// and accordingly, trigger routing navigation in your app
// (typically, you would put it in a separated file)

export function isNavigationPath(path: string) {
  return !!path && !path.startsWith('javascript:void');
}

export function isExternalPath(path: string) {
  return /^https?:\/\//.test(path);
}

export function isApplicationPath(path: string) {
  return isNavigationPath(path) && !isExternalPath(path);
}

document.body.addEventListener('click', (evt) => {

    const target = evt.target as Element;
    let link: Element;

    if (target.nodeName === 'A') {
      link = target;
    } else {
      link = target.closest('a');
      if (!link) {
        return;
      }
    }

    const url = link.getAttribute('href');

    // do nothing if it is not an app's internal link
    if (!isApplicationPath(url)) {
      return;
    }

    // avoid the default browser's behaviour when clicking on a link
    // (i.e. do not reload the page).
    evt.preventDefault();

    // it is a normal app's link, so trigger the routing navigation
    router.push(url);
  });

see more advanced usages in the unit tests.

Package Sidebar

Install

npm i prouter

Weekly Downloads

9

Version

10.0.26

License

MIT

Unpacked Size

44.6 kB

Total Files

10

Last publish

Collaborators

  • rogerpadilla