@surface/web-router
TypeScript icon, indicating that this package has built-in type declarations

1.0.0-dev.202209222047 • Public • Published

Summary

Introduction

@surface/web-router enables navigation in your application without the need of page refresh. Features support for synchronous and asynchronous routes, url templates and native dependency injection.

Basic setup

import HTMLXElement               from "@surface/htmlx-element";
import type { RouteConfiguration } from "@surface/web-router";
import WebRouter                   from "@surface/web-router";

// <router-outlet> determines where the content will be outputed.
@element("app-root", { template: "<router-outlet></router-outlet>" })
class App extends HTMLXElement
{ }

document.body.appendChild(new App());

// Component used as view.
@element("home-view")
class HomeView extends HTMLXElement
{ }

// The router will start looking for the outlet at the shadow root of the app-root component.
const router = new WebRouter({ root: "app-root", routes: [{ component: HomeView, path: "/" }] });

// Navigates to /home.
void router.push("/home");

Optionally, you can also provide a base URL.

const router = new WebRouter({ root: "app-root", baseUrl: "my-app", routes: [{ component: HomeView, path: "/" }] });

// Resolves to "my-app/home" on browser.
void router.push("/home");

By default, the router lookups by <router-outlet> element where it outputs the resolved component. This can overrided at configuration level passing the selector option.

import HTMLXElement               from "@surface/htmlx-element";
import type { RouteConfiguration } from "@surface/web-router";
import WebRouter                   from "@surface/web-router";

@element("app-root", { template: "<div class='outlet'></div>" })
class App extends HTMLXElement
{ }

document.body.appendChild(new App());

// Component used as view.
@element("home-view")
class HomeView extends HTMLXElement
{ }

// The router will start looking for the outlet at the shadow root of the app-root component.
const router = new WebRouter({ root: "app-root", routes: [{ component: HomeView, path: "/", selector: "div.outlet" }] });

// Navigates to /home.
void router.push("/home");

Route Configuration

Route configuration allow us to create routes in some ways.

Synchronous

Component impoted in synchronous way.

import type { RouteConfiguration } from "@surface/web-router";
import HomeView                    from "./views/home-view";

const routes: RouteConfiguration[] =
[
    { component: HomeView, path: "/" },
    { component: () => HomeView, path: "/" }, // Lazy, but still synchronous.
];

Asynchronous

Also known as lazy loading. Component is resolved on demand and can be used for code spliting.

const routes: RouteConfiguration[] =
[
    { component: async () => import("./views/home-view"), path: "/" }, // Resolve to default export
    { component: async () => (await import("./views/home-view")).MyView, path: "/" }, // Using explicit export
];

Named

Named allow us navigate to the route using the name instead the path.

Note that when the route contains mandatory parameters it must also be specified otherwise the route will not match (see).

const routes: RouteConfiguration[] =
[
    { component: async () => import("./views/home-view"), path: "/", name: "home" },
    { component: async () => import("./views/home-view"), path: "/foo/{id}", name: "foo" },
];

// Matches: /
void router.push({ name: "home" });

// Matches: foo/1
void router.push({ name: "foo", parameters: { id: 1 } });

// No matches
void router.push({ name: "foo" });

Metadata

Meta is static data provided aside the route that can be access through current route instance.

const routes: RouteConfiguration[] =
[
    { component: async Home, path: "/", meta: { requiresAuth: false } },
];

// ...
console.log(router.route.meta); // outputs { requiresAuth: false }

Children Route

When using children routes the router only updates the component related to the segment that has changed keeping state of the parent segment.

const routes: RouteConfiguration[] =
[
    {
        children:
        [
            {
                component: ChildrenA,
                path:      "children-a",
            },
            {
                component: ChildrenB,
                path:      "children-b",
            },
        ],
        component: View,
        path:      "/view",
    }
];

// Outputs View component inside root outlet
void router.push("/view");

// View dont changes. Outputs ChildrenA component inside View outlet;
void router.push("/view/children-a");

// View dont changes. Outputs ChildrenB component inside View outlet.
void router.push("/view/children-b");

Multiples Components

One component can contains multiples named outlets handled by one single route. The property components allows specifies where each component outputs.

const routes: RouteConfiguration[] =
[
    {
        components:
        {
            default: Home,        // <router-outlet></router-outlet>
            details: HomeDetails, // <router-outlet name="details"></router-outlet>
        },
        path: "/",
    }
];

// Outputs View component inside root outlet
void router.push("/view");

// View dont changes. Outputs ChildrenA component inside View outlet;
void router.push("/view/children-a");

// View dont changes. Outputs ChildrenB component inside View outlet.
void router.push("/view/children-b");

Url Templates

Url Templates can be used to provide additional information about the route.

Parameters

The most simple example is url parameters.

const routes: RouteConfiguration[] =
[
    { components: User, path: "/path-1/{id}" },
    { components: User, path: "/path-2/{id?}" }, // Parameter also can be optional
    { components: User, path: "/path-3/{id=0}" }, // Sets a default value when the optional segment is omited.
];

When the router or the browser navigate to the some of the mapped URLs, the router's active route will have the mapped parameter available.

The URL http://localhost/path-1/42 will produces some like:

{ id: "42" }

The URL http://localhost/path-2 will produces some like:

{ }

The URL http://localhost/path-3 will produces some like:

{ id: "0" }

Constraints

Constrains are used to restrict match to exact pattern expected by parameter. The builtin constraints are: UUID, Alpha, Number, Boolean and Date.

const routes: RouteConfiguration[] =
[
    { components: User, path: "/user/{id:Alpha}" },
];

Using above configuration.

The URL https://localhost/user/xyz will match, while https://localhost/user/41 will not.

Transformers

By default all parameters are captured like string. But if you need some kind of transformation, transformers can be used.

The builtin transformer are: Number, Boolean and Date.

const routes: RouteConfiguration[] =
[
    { components: User, path: "/user/{id:Number}" },
    { components: User, path: "/user/{id:Number?}" } // Optional,
    { components: User, path: "/user/{id:Number=0}" } // With Default value,
];

The URL http://localhost/user/42 will produces some like:

{ id: 42 }

Custom Constraints and Transformers

You also can provide your own custom constraints implementeing the interface IRouteParameterConstraints and/or custom transformers implementeing the interface IRouteParameterTransformers` and registering on the router instance.

import type { IRouteParameterConstraint, IRouteParameterTransformer } from "@surface/web-router";
import WebRouter                                                      from "@surface/web-router";

const arrayTransformer: IRouteParameterConstraint & IRouteParameterTransformer =
{
    parse:     value => value.split(","),
    stringify: value => value.join(),
    validate:  value => value.includes(","),
};

const routes: RouteConfiguration[] =
[
    { components: User, path: "/user/{id:Array}" },
];

const router = new WebRouter({ root: "app-root", routes, transformers: { Array: arrayTransformer } });

Match All

Matches any segment between placeholder patterns.

const routes: RouteConfiguration[] =
[
    { components: User, path: "/user/{*rest}/details" },
];

The URL http://localhost/user/some/deep/path/details/ will produces some like:

console.log(router.route.parameters); // outputs { rest: "some/deep/path" }

Interceptors

Interceptors can be used to guard some routes and re-route when needed.

import type { IRouterInterceptor } from "@surface/web-router";
import { authService }             from "./singletons";

class Interceptor implements IRouterInterceptor
{
    public async intercept(next: (route: string | NamedRoute) => Promise<void>, to: Route, from: Route | undefined): Promise<void>
    {
        if (to.meta.requireAuth && !authService.authenticated)
        {
            // Re-routes to login if not authenticated.
            await next("/login");
        }
    }
}

const routes: RouteConfiguration[] =
[
    { component: async () => import("./views/home"),  path: "/", meta: { requireAuth: true } },
    { component: async () => import("./views/login"), path: "/login" },
];

const router = new WebRouter({ root: "app-root", interceptors: [Interceptor], routes });

Life Cycle Hooks

Besides interceptor, some hooks also can be used to handle some route life cycles.

export default interface IRouteableElement extends HTMLElement, Partial<IDisposable>
{
    // Called when entering on new route
    onRouteEnter?(to: Route, from?: Route): void;
    // Called when leaving on new route
    onRouteLeave?(to: Route, from?: Route): void;
    // Called when the route parameters changes
    onRouteUpdate?(to: Route, from?: Route): void;
    // Called when the element is discarded
    dispose?(): void;
}

Navigation

It is possible to navigate between routes using url, named routes or router history.

const routes: RouteConfiguration[] =
[
    { component: async () => import("./views/company"), path: "/company/{id}", name: "company" }
];

// Pushs to window.location.href
await router.pushCurrentLocation();

await router.push("/company/1?show-employees=true#details");
// Equivalent to
await router.push({ name: "company", parameters: { id: 1 }, query: { "show-employees": "true" }, hash: "details" });

// Go back one step in history
await router.back();

// Advance one step in history.
await router.forward();

// Go back two steps in history
await router.go(-2);

// Advance two steps in history.
await router.go(2);

// Replaces current entry in history
await router.replace("/company/2");

Dependency Injection

@surface/web-router has builtin support of dependency injection through of module @surface/dependency-injection and can inject dependencies directly on routed component constructor or properties (notes that injected properties are initialized after constructor). For code split, see Provider.

import HTMLXElement               from "@surface/htmlx-element";
import Container, { inject }       from "@surface/dependency-injection";
import type { RouteConfiguration } from "@surface/web-router";
import WebRouter                   from "@surface/web-router";

@element("home-view")
class HomeView extends HTMLXElement
{
    @inject(MyOtherService)
    private readonly otherService!: MyOtherService;

    public constructor(@inject(MyService) private readonly service: MyService)
    {
        super();
    }
}

const container = new Container();

container.registerSingleton(MyService);
container.registerSingleton(MyOtherService);

const router = new WebRouter({ root: "app-root", container, routes: [{ component: HomeView, path: "/" }] });

// Creates the app element using the container and then routes to the current location
void import("./app")
    .then(x => document.body.appendChild(container.inject(x.default)))
    .then(() => void router.pushCurrentLocation());

Directive

@surface/web-router provides a custom directive that allows you to create router links directly in html and accepts the same arguments that router.push(...).

import HTMLXElement                      from "@surface/htmlx-element";
import WebRouter, { RouterLinkDirective } from "@surface/web-router";
import routes                             from "./routes";

const router = new WebRouter({ root: "app-root", routes });

// Register directive
HTMLXElement.registerDirective("router-link", context => new RouterLinkDirective(router, context));

void import("./app").then(() => void router.pushCurrentLocation());
<a #router-link="'/components/users'">Users</a>
<a #router-link="{ name: 'companies' }">Companies</a>

Package Sidebar

Install

npm i @surface/web-router

Weekly Downloads

5

Version

1.0.0-dev.202209222047

License

MIT

Unpacked Size

35.6 kB

Total Files

21

Last publish

Collaborators

  • hitalloexiled