# prouter

[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/rogerpadilla/prouter/blob/main/LICENSE)
[![tests](https://github.com/rogerpadilla/prouter/actions/workflows/tests.yml/badge.svg)](https://github.com/rogerpadilla/prouter)
[![coverage status](https://coveralls.io/repos/github/rogerpadilla/prouter/badge.svg)](https://coveralls.io/github/rogerpadilla/prouter)
[![npm version](https://badge.fury.io/js/prouter.svg)](https://www.npmjs.com/prouter)

Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of [express middlewares](https://expressjs.com/en/guide/writing-middleware.html).

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](https://github.com/rogerpadilla/prouter/blob/master/src/browser-router.spec.ts#L7) 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](https://github.com/pillarjs/path-to-regexp) (so it allows the same flexibility to declare routes). Read more about the concept of middlewares [here](https://expressjs.com/en/guide/writing-middleware.html).
- **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](https://developer.mozilla.org/en-US/docs/Web/API/History_API) API for routing.
- Unit tests for every feature are created.

Do you like Prouter? [please give it a 🌟](https://github.com/rogerpadilla/prouter)

## Installation

```bash
# 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

```js
// 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

```js
// 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

```js
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

```js
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

```js
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.](https://github.com/rogerpadilla/prouter/blob/master/src/browser-router.spec.ts)
