# @wroud/navigation

[![ESM-only package][package]][esm-info-url]
[![NPM version][npm]][npm-url]

[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
[esm-info-url]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[npm]: https://img.shields.io/npm/v/@wroud/navigation.svg
[npm-url]: https://npmjs.com/package/@wroud/navigation

@wroud/navigation is a flexible, pattern-matching navigation system for JavaScript applications. It provides a framework-agnostic routing solution with powerful pattern matching capabilities, browser integration, and navigation state management.

## Features

- **Pattern-based Routing**: Built-in support for static routes, parameter segments (`/users/:id`), wildcard patterns (`/files/:path*`), and query parameters (`/search?q=:query&page=:page<number>`).
- **Framework Agnostic**: Works with any JavaScript framework or vanilla JS.
- **Type Safety**: Written in TypeScript with full type inference for route parameters, including typed parameters (`<number>`, `<boolean>`, `<date>`, `<json>`), optional/required query params (`!` suffix), and wildcard arrays.
- **Navigation History**: Built-in navigation history management.
- **Browser Integration**: Optional browser URL synchronization.
- **Extensible**: Create custom navigation implementations for any environment.
- [Pure ESM package][esm-info-url]

## Installation

Install via npm:

```sh
npm install @wroud/navigation
```

Install via yarn:

```sh
yarn add @wroud/navigation
```

## Documentation

For detailed usage and API reference, visit the [documentation site](https://wroud.dev).

## Examples

### Basic Usage

```ts
import { Navigation, Router } from "@wroud/navigation";

// Create a router with default settings
const router = new Router();

// Add routes
router.addRoute({ id: "/" });
router.addRoute({ id: "/users" });
router.addRoute({ id: "/users/:id" });

// Create a navigation instance with the router
const navigation = new Navigation(router);

// Navigate to a route
await navigation.navigate({ id: "/users/:id", params: { id: "123" } });

// Get current route state
const currentState = navigation.getState();
console.log(currentState); // { id: "/users/:id", params: { id: "123" } }

// Go back to previous route
await navigation.goBack();
```

### Using Pattern Matching

```ts
import { Navigation, Router, TriePatternMatching } from "@wroud/navigation";

// Create a router with pattern matching
const router = new Router({
  matcher: new TriePatternMatching({ trailingSlash: false })
});

// Add routes
router.addRoute({ id: "/" });
router.addRoute({ id: "/app" });
router.addRoute({ id: "/app/users" });
router.addRoute({ id: "/app/users/:id" });

// Create a navigation instance
const navigation = new Navigation(router);

// Match a URL to a route
const match = router.matchUrl("/app/users/123");
console.log(match); // { id: "/app/users/:id", params: { id: "123" } }

// Build a URL from a route and parameters
const url = router.buildUrl("/app/users/:id", { id: "456" });
console.log(url); // "/app/users/456"

// Navigate using the pattern matching
await navigation.navigate(router.matchUrl("/app/users/789"));
```

### Query Parameters and Typed Params

```ts
import { Router, TriePatternMatching } from "@wroud/navigation";

const router = new Router({
  matcher: new TriePatternMatching({ trailingSlash: false })
});

// Query params are optional by default, use ! to mark as required
router.addRoute({ id: "/search?q=:query!&page=:page<number>&sort=:sort" });

// Encode — optional params are omitted when not provided
const url = router.buildUrl("/search?q=:query!&page=:page<number>&sort=:sort", {
  query: "hello",
  page: 2,
});
console.log(url); // "/search?q=hello&page=2"

// Decode — typed params are automatically converted
const params = router.matchUrl("/search?q=hello&page=2");
console.log(params); // { id: "...", params: { query: "hello", page: 2 } }
```

### Browser Integration

```ts
import { Navigation, Router, TriePatternMatching } from "@wroud/navigation";
import { BrowserNavigation } from "@wroud/navigation/browser";

// Create a router with pattern matching
const router = new Router({
  matcher: new TriePatternMatching({
    trailingSlash: true,
    base: '/app' // Base path for the application
  })
});

// Add routes
router.addRoute({ id: "/" });
router.addRoute({ id: "/products" });
router.addRoute({ id: "/products/:id" });
router.addRoute({ id: "/blog/:year/:month/:slug" });

// Create a navigation instance
const navigation = new Navigation(router);

// Add a navigation listener for all navigation events
// including the initial navigation
const unsubscribe = navigation.addListener((type, from, to) => {
  if (type === "navigate" && !from) {
    console.log("Initial navigation:", to);
  } else {
    console.log(`Navigation: ${type}`, { from, to });
  }
});

// Create a browser navigation instance
const browserNavigation = new BrowserNavigation(navigation);

// Initialize browser navigation by registering routes
// This will sync with the browser's URL and history
await browserNavigation.registerRoutes();

// Navigate - this will update the browser URL
await navigation.navigate({ 
  id: "/products/:id", 
  params: { id: "123" } 
});

// Browser URL is now /app/products/123

// Later, remove the listener
unsubscribe();
```

### Protected Routes with Navigation Guards

```ts
import { Navigation, Router } from "@wroud/navigation";

// Create a router
const router = new Router();

// Add routes with navigation guards
router.addRoute({ 
  id: "/", 
});

router.addRoute({ 
  id: "/dashboard", 
  // Check if user is authenticated before activating this route
  canActivate: (to, from) => {
    return isAuthenticated(); // Your authentication check
  }
});

router.addRoute({ 
  id: "/editor/:documentId", 
  // Check if user has permission to edit this document
  canActivate: async (to, from) => {
    if (!to.params.documentId) return false;
    return await hasEditPermission(to.params.documentId);
  },
  // Ask for confirmation before navigating away
  canDeactivate: (to, from) => {
    return confirm("Discard unsaved changes?");
  }
});

const navigation = new Navigation(router);

// This will fail if not authenticated
await navigation.navigate({ id: "/dashboard", params: {} });
```

## Changelog

All notable changes to this project will be documented in the [CHANGELOG](./CHANGELOG.md) file.

## License

This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. 
