npm package: @merzin/router
Description: Router for web components and vanilla function components.
Latest version: 0.5.3

# Router

Router for web components and vanilla function components.

## Defining Routes

Routes are defined using the `defineRoute` function. Function `defineRoute`
takes two (2) arguments, a pathname or hash and a
[`Factory<HTMLElement>`](https://www.npmjs.com/package/@merzin/factory). You can
use it to map a pathname or a hash to a web component or a vanilla function
component.

### Web Component

```typescript
import { defineRoute } from "@merzin/router";

class LandingPage extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = "<h1>Welcome!</h1>";
  }
}
customElements.define("landing-page", LandingPage);

defineRoute("/", LandingPage);
// You can also use the same instance always by instantiating the component
// defineRoute("/", new LandingPage());
```

### Vanilla Function Component

```typescript
import { defineRoute } from "@merzin/router";

function LandingPage() {
  const div = document.createElement("div");
  div.innerHTML = "<h1>Welcome!</h1>";
  return div;
}

defineRoute("/", LandingPage);
// You can also use the same instance always by instantiating the component
// defineRoute("/", LandingPage());
```

### Parameterized Routes

You can define parameterized routes using the `:param` syntax. The parameters
object is passed to the component as an argument.

```typescript
import { defineRoute } from "@merzin/router";

// Parameterized Web Component
class HelloPage extends HTMLElement {
  constructor({ name }: Record<string, string>) {
    super();
    this.innerHTML = `<h1>Hello, ${name}!</h1>`;
  }
}
customElements.define("hello-page", HelloPage);

// Parameterized Vanilla Function Component
function GoodbyePage({ name }: Record<string, string>) {
  const div = document.createElement("div");
  div.innerHTML = `<h1>Goodbye, ${name}!</h1>`;
  return div;
}

defineRoute("/hello/:name", HelloPage);
defineRoute("/goodbye/:name", GoodbyePage);
```

> Note: Make sure you match the trailing slash in the pathname if you want to
> define a route with a trailing slash. For example, `"/hello/:name/"` will not
> match `"/hello/John"` but will match `"/hello/John/"`.

### Modals

Modals are defined by providing a hash instead of a pathname. The hash should
only start with "#" and otherwise not include "#" as it is used as a delimiter
to allow rendering multiple modals at once.

```typescript
import { defineRoute } from "@merzin/router";

class LoginModal extends HTMLElement {
  constructor() {
    super();
    this.style.position = "fixed";
    this.innerHTML = "<h1>Login</h1>";
  }
}
customElements.define("login-modal", LoginModal);

defineRoute("#login", LoginModal);
```

## Starting Router

To start the router, you need to call the `startRouter` function. It takes an
optional argument which is the root element where the router will render the
components. If not provided, it will use `document.body` as the root element.

```typescript
import { startRouter } from "@merzin/router";

startRouter(document.getElementById("root")!);
```

The optional argument can also be an object with properties `viewRoot` and
`modalRoot` if you need to render view and modals in different places.

```typescript
import { startRouter } from "@merzin/router";

startRouter({
  viewRoot: document.querySelector("main")!,
  modalRoot: document.body,
});
```

## Extended History API

This library extends the browser history API to provide a way to listen to URL
changes and to manage history stack.

### Navigating

You can navigate using browser history API. Anchor links are intercepted and use
`history.pushState` if href is same origin and target is not "\_blank".

```typescript
history.pushState({}, "", "/hello/John");
```

Pushing to modal stack can be done by appending to the hash.

```typescript
history.pushState({}, "", location.hash + "#login");
```

Navigating forwards and backwards can be done with `history.forward()` and
`history.back()`. This library extends these methods to accept an optional
fallback URL. Calling without argument will result in the default behavior.

When calling `history.back("/fallback")`, it will check if there is a previous
history entry with the same origin. If there is, it will navigate to that entry.
If not, it will call `history.replaceState({}, "", "/fallback")` instead. This
is useful for ensuring that the user stays on the same page. If you don't want
to provide a fallback URL but still want to prevent navigating away from the
current origin, you can use `history.back(true)`.

Similarly when calling `history.forward("/fallback")`, it will check if there is
a next history entry with the same origin. If there is, it will navigate to that
entry. If not, it will call `history.pushState({}, "", "/fallback")` instead.
Calling `history.forward(true)` will do nothing if there is no next same origin
history entry.

### State Change Event

By default we can listen to "popstate" event to detect URL changes. This event
is fired when navigating in the history stack using the browser's back and
forward buttons, or when calling `history.back()`, `history.forward()`, or
`history.go()`. However, there is no event fired when calling
`history.pushState()` or `history.replaceState()`. This library extends these
methods to provide a "statechange" event that is fired always when the URL
changes. Class `StateChangeEvent` contains the following properties:

- `state`: The state object passed to `history.pushState()` or
  `history.replaceState()`.
- `delta`: The amount moved in the history stack. Positive values indicate
  moving forward, negative values indicate moving backward, and zero indicates
  no movement i.e. replacement.
- `index`: The current index in the history stack.

### History Stack

This library extends browser history API with `history.stack` readonly property.
It represents the current session history stack and is of type `HistoryStack`.

```typescript
interface HistoryStack {
  index: number; // current position in the items list
  items: HistoryStackItem[]; // list of history items
}

interface HistoryStackItem {
  href: string; // the URL string without origin, i.e. pathname, hash and search
  state: { __index: number; [key: string]: any };
}
```

As you may notice `HistoryStackItem` has a `__index` property. This is injected
in `history.pushState` and `history.replaceState` calls to allow the router to
keep track of the history stack during "popstate" event.

## On Back

You can bind a function to be called when the user navigates back in the history
stack with `onBack()`. This function pushes current URL to the stack and
whenever user navigates to an entry before it the callback gets called. This is
useful for handling cases where you want to perform some action when the user
navigates back, such as clearing selection or hiding context menu.

```typescript
import { onBack } from "@merzin/router/back";

onBack(() => {});
```

> Function `onBack()` is not recommended for modals as after resolving it leaves
> a dangling history entry.