~vladh/shrub-router

23e13b5bdb2842ba25146bac946e948a07e435c1 — Vlad-Stefan Harbuz 9 months ago 0c39950
simplify API and finish README
2 files changed, 44 insertions(+), 48 deletions(-)

M README.md
M shrub-router.js
M README.md => README.md +38 -25
@@ 2,10 2,11 @@

![A cartoon illustration of a fruit-bearing bush](images/hawaii_ohelo_berry.png)

Shrub is a minimal clientside router that still works without Javascript. Shrub is only 3KB gzipped and allows you to:
Shrub is a minimal clientside router that gracefully degrades without Javascript. Shrub is only 3KB gzipped and allows
you to:

* implement clientside navigation with almost no configuration,
* preserve full server side routing so that everything still works without Javascript,
* fall back to full server side routing so that everything still works without Javascript,
* keep all your HTML on the server side and avoid bundling your HTML into your `.js` files, and
* avoid using a large framework like vue-router.



@@ 26,49 27,61 @@ the `href` on an `<a>`, or the `action` on a `<form>`. Shrub will then make a re

When using a clientside router, it's usually desirable to be able to only navigate the main content of the page, not to
replace the HTML of the entire page. Shrub does this by identifying the body of the old page and the body of the new
page, and replacing the former with the latter. Shrub does this by looking for an element with a specific node name, by
default `<main>`. Therefore, everything in the old `<main>` will be replaced with the new `<main>` from the fetched
page. This means that you don't need to serve the body of the page at some specific template URL — Shrub will simply
make a request for the new page at the usual URL the user will navigate to. This behaviour can be customised — see
“Customising Content Transformation” below.

TODO: Get rid of `.dynamic-scripts` by reloading scripts in place.
page, and replacing the former with the latter. Shrub does this by looking for a `<main>` element — remember that there
can only be one `<main>` tag per page. Therefore, everything in the old `<main>` will be replaced with the new `<main>`
from the fetched page. This means that you don't need to serve the body of the page at some specific template URL —
Shrub will simply make a request for the new page at the usual URL the user would normally navigate to.

## Usage

First, `npm install shrub-router`. You can then import and initialise it:

```
import ShrubRouter from 'shrub-router'
ShrubRouter.addView('/');
ShrubRouter.addView('/admin');
ShrubRouter.addView('/admin/tracks');
ShrubRouter.addView('/admin/track/:id');
ShrubRouter.addView('/search');
ShrubRouter.setTransformContent(pickMain);
ShrubRouter.init();
```

You need to tell Shrub about which routes you want to be handled. Any routes that you don't tell Shrub about will not be
intercepted. When the user clicks on these non-intercepted routes, the browser will navigate normally, reloading the
page.

To add routes that replace the page body with the fetched page, use `ShrubRouter.add()`:

```
ShrubRouter.add('/');
ShrubRouter.add('/about');
ShrubRouter.add('/about/details');
```

If you want to perform a custom action when a route is navigated to, specify a handler to replace the default one:

```
ShrubRouter.add('/contact', () => {
	document.write("Hello world");
});
```

You can remove a route with `ShrubRouter.remove('/contact')`.

There is one important thing to note related to the initialisation of Javascript included in the page. Normally, you
would want to run your Javascript code when the page has loaded, by adding a listener for `DOMContentLoaded` or by
placing your code at the bottom of your HTML. When using Shrub, you obviously want your code to be not only on the
initial page load, but also when a page is navigated to. Therefore, you should place all of your in-page Javascript code
within an event listener for `shrub:loaded`:

```
window.addEventListener('shrub:loaded', () => {
	PetiteVue.createApp({
		Inputset,
		track: !{JSON.stringify(track)},
		keywords: !{JSON.stringify(keywords)},
		tracks: !{JSON.stringify(tracks)},
		name: 'Timmy',
	}).mount();
});
```

## Useful Notes

There are various ways in which you can customise Shrub's behaviour.

### Customising Server Side Behaviour

When making requests for pages using `fetch()`, Shrub adds a `Shrub-Router: true` header, which you can use to add
custom behaviour for requests made by Shrub on the server side.

### Customising Content Transformation

## Limitations

The interception of click events could be further improved:

M shrub-router.js => shrub-router.js +6 -23
@@ 2,7 2,6 @@

let routes = [];
let contentNodeName = 'main';
let dynamicScriptContainerSelector = '.dynamic-scripts';

function transformContent(html) {
    const openLoc = html.indexOf(`<${contentNodeName}`);


@@ 183,10 182,6 @@ function interceptNavigation() {
}

function renderContent(content) {
    const elDynamicScriptContainer = document.querySelector(
        dynamicScriptContainerSelector);
    elDynamicScriptContainer.innerHTML = '';

    const elTarget = document.querySelector(contentNodeName);
    elTarget.outerHTML = content;
    const elNew = document.querySelector(contentNodeName);


@@ 201,8 196,7 @@ function renderContent(content) {
    elNew.querySelectorAll('script').forEach((el) => {
        const elNewScript = document.createElement('script');
        elNewScript.innerHTML = el.innerHTML;
        el.remove();
        elDynamicScriptContainer.appendChild(elNewScript);
        el.replaceWith(elNewScript);
    });

    const elAutofocusTarget = elNew.querySelector('[autofocus]');


@@ 254,15 248,14 @@ async function viewHandler({ route, path, method, data, routeParams }) {
    renderContent(transformContent(content));
}

function add(path, handler, options) {
function add(path, handler) {
    if (!handler) {
        handler = viewHandler;
    }
    if (routes.find(function(r) { return r.path === path; })) {
        throw new Error('Tried to add route, but one already exists with the same path');
    }
    routes.push({ path, handler, options });
}

function addView(path, options) {
    add(path, viewHandler, { ...options });
    routes.push({ path, handler });
}

function remove(path) {


@@ 285,17 278,7 @@ async function init() {
}

export default {
    setContentNodeName(val) {
        contentNodeName = val;
    },
    setDynamicScriptContainerSelector(val) {
        dynamicScriptContainerSelector = val;
    },
    setTransformContent(val) {
        transformContent = val;
    },
    add,
    addView,
    remove,
    init,
};