suunta
TypeScript icon, indicating that this package has built-in type declarations

3.5.1 • Public • Published

Title Image

Suunta

A simple SPA routing and state management library for everyday use

Demo

For an interactive demo, visit ReplIt

Table of Contents

Install

npm install suunta

Usage

Suunta doesn't pack any dependencies, and therefore doesn't bring it's own rendering library either.

The easiest way to get started is to install lit and create a renderer with that as shown below.

import { FooView } from "./foo";
import { html, render } from "lit";

const routes: Route[] = [
    {
        path: "/",
        view: html`<p>Hello world!</p>`
    },
    {
        path: "/foo",
        view: FooView,
        title: "Example - Foo View"
    }
];

const renderer = (view, route, renderTarget) => {
    render(html`${view}`, renderTarget);
};

const routerOptions: SuuntaInitOptions = {
    routes,
    renderer,
    target: document.body
};

const router = new Suunta(routerOptions);

router.start();

Dynamic routes

Suunta supports dynamic routes with the {keyword}-notation. If you want the matching to only match certain types of data, you can supply a regex for the matcher.

You can access properties of your dynamic routes with router.getCurrentView()?.route.properties?.id

const routes: Route[] = [
    {
        path: "/",
        name: "Home",
        view: html`<p id="needle">Hello world!</p>`
    },
    {
        path: "/user",
        name: "User",
        view: html`<p>User page</p>`
    },
    {
        path: "/user/{id}(\\d+)",
        name: "User profile",
        view: () => html`<p>User page for id ${router?.getCurrentView()?.properties.id}</p>`
    },
    {
        path: "/search/{matchAll}",
        name: "Search",
        view: html`<p>Search page for ${router?.getCurrentView()?.properties.matchAll}</p>`
    },
    {
        path: "/user/{id}(\\d+)/search/{matchAll}",
        name: "User profile with search",
        view: () => html`
            <p>User page for id ${router?.getCurrentView()?.properties.id}</p>
            <p>Search page for ${router?.getCurrentView()?.properties.matchAll || "Nothing"}</p>
        `
    },
    {
        path: "/{notFoundPath}(.*)",
        name: "404",
        view: html`<p>Page not found</p>`
    },
    {
        path: "/redirect",
        name: "Redirect",
        redirect: "Home"
    }
];

const routerOptions: SuuntaInitOptions = {
    routes,
    target: "#outlet",
    renderer: litRenderer
};

router = new Suunta(routerOptions);
return router;

State

A lot of views have state. And that state can change, and so should the page content with it.

For state management, Suunta provides a createState hook, which will take the initial state of your view as a parameter.

When any of the values of that state object is directly manipulated, the view will update accordingly.

import { html } from 'lit';
import { createState } from 'suunta';

export const View = () => {
    const state = createState({
        count: 0,
    });

    const addCount = () => {
        state.count += 1;
    };

    return () => html`
        <p>Foo View</p>
        <p>Count: ${state.count}</p>
        <button @click=${addCount}>Count++</button>
    `;
};

Global State

For some use cases you might want to have state that is shared between multiple views, and is also reactive.

For these cases, the use of the createGlobalState hook is recommended.

This hook should not be used to replace the createState hook, but to implement those features, where a shared reactive state is useful for the productivity and efficiency of the application.

When values of globalState objects are updated, all of the views managed by the current Suunta instance will be updated.

For most applications only using a single view at a time, this won't affect performance, but for views with subviews through the Child Routes, this will cause an performance hit.

// ../index.js
import { createGlobalState } from "suunta";

export const globalState = createGlobalState({
    count: 0
})


// FooView.js
import { html } from 'lit';
import { createState } from 'suunta';
import { globalState } from "../index.js";

export const View = () => {
    const addCount = () => {
        globalState.count += 1;
    };

    return () => html`
        <p>Foo View</p>
        <p>Count: ${globalState.count}</p>
        <button @click=${addCount}>Count++</button>
    `;
};

Named routes

With Suunta, you don't have to go through the hassle of going through your whole codebase with CTRL - F after changing a route.

You can define your routes using the pathByRouteName function and generate routes dynamically by the name of said route.

const routes = [
    {
        name: "Home",
        path: "/",
        view: HomeView
    },
    {
        name: "UserView",
        path: "/users/{userId}",
        view: UserView
        children: [
            {
                name: "UserAttendances",
                path: "/attendances/{attendanceId}",
                view: UserAttendanceView
            }
        ]
    },
]

const homeView = router.pathByRouteName("Home");
// > homeView => /


const userView = router.pathByRouteName("UserView", 123);
// > userView => /users/123

const attendanceView = router.pathByRouteName("UserAttendances", 123, "suunta-course");
// > attendanceView => /users/123/attendances/suunta-course

html`<a href="${router.pathByRouteName("UserView", 123)}">To user view</a>`

Redirects

Supplying redirects is as easy as adding a redirect property onto your route, and targetting another view by name with it.

const routes: Route[] = [
    {
        path: "/",
        name: "Home",
        view: html`<p id="needle">Hello world!</p>`
    },
    {
        path: "/redirect",
        name: "Redirect",
        redirect: "Home"
    }
]

Not Found -pages

Providing a 404 page for you application is done by creating a all-matching wildcard route, and placing it at the bottom of your route list.

const routes: Route[] = [
    {
        path: "/",
        name: "Home",
        view: html`<p id="needle">Hello world!</p>`
    },
    {
        path: "/{notFoundPath}(.*)",
        name: "404",
        view: html`<p>Page not found</p>`
    },
]

Not Found -pages with redirect

You can also make your 404 pages a redirect

const routes: Route[] = [
    {
        path: "/",
        name: "Home",
        view: html`<p id="needle">Hello world!</p>`
    },
    {
        path: "/{notFoundPath}(.*)",
        name: "404",
        redirect: "Home"
    },
]

Dynamic imports

For cases where you have a bunch of views and want to squeeze out some extra performance from your packages, you can package split your code by dynamically importing your routes.

Suunta will handle the rest.

// ./views/foo.js
import { html } from "lit";

export const FooView = () => html`<p id="needle">
    Foo bar
</p>`;

// router.js
import { BarView } from "./views/bar.js";

const FooView = () => import("./views/foo.js");

const routes: Route[] = [
    {
        path: "/",
        name: "Home",
        view: html`<p id="needle">Hello world!</p>`
    },
    {
        path: "/foo",
        name: "Foo",
        view: FooView
    },
    {
        path: "/bar",
        name: "Bar",
        view: BarView
    },
];

const routerOptions: SuuntaInitOptions = {
    routes,
    target: "#outlet"
};

router = new Suunta(routerOptions);

Rendering into outlets

By using a <suunta-view> pseudoelement, you can tell Suunta to render the wanted content to a said location on page.

<body>
    <suunta-view></suunta-view>
</body>

Sub-views

The <suunta-view> outlet can be especially useful for rendering sub-views. If you want your view to have a navigatable sub-view, meaning that you want the view to render, without it un-rendering the previous view, you can do that utilizing the suunta-view element and child routes

const routes: Route[] = [
    {
        path: '/',
        name: 'Home',
        view: HelloView    
    },
    {
        path: '/sub',
        view: SubView,
        children: [
            {
                path: '/sub',
                view: SubView,
                children: [
                    {
                        path: '/sub',
                        view: SubView,
                        children: [
                            {
                                path: '/sub',
                                view: SubViewFloor,
                            },
                        ],
                    },
                ],
            },
        ],
    },
}

export function SubView() {
    return () => html`
        <p>
            This is a view. By adding a child view to this view, and appending a
            <code>&ltsuunta-view&gt</code> container into it, we can render subviews
        </p>

        <a href="${window.location.href}/sub">Deeper</a>

        <suunta-view></suunta-view>
    `;
}

By navigating to /sub/sub/sub/sub, we get a DOM looking like this:

<body>
    <p>This is a view...</p>

    <a href="/sub/sub">Deeper</a>

    <suunta-view>
        <p>This is a view...</p>

        <a href="/sub/sub/sub">Deeper</a>

        <suunta-view>
            <p>This is a view...</p>

            <a href="/sub/sub/sub/sub">Deeper</a>

            <suunta-view>
                <p>This is a subview floor</p>
            </suunta-view>

        </suunta-view>

    </suunta-view>
</body>

And when navigating backwards, only the subviews are un-rendered. The whole page does not require a refresh.

Hooks

Suunta provides lifecycle hooks to plug into the navigation phases from within your own views.

import { html } from "lit-html";
import { createState, onNavigated, onUpdated } from "suunta";

export function HomeView() {

    const state = createState({
        count: 0
    });
    
    // Triggers whenever a navigation has occured
    onNavigated(() => {
        console.log("HomeView rendered");
    });

    // Triggers whenever the current view's state object's value is updated
    // e.g. when state.count is incremented
    onUpdated((name, oldValue, newValue) => {
        console.log('Update', { name, oldValue, newValue });
    });

    return () => html`
        <button @click=${() => state.count += 1}>Clicked ${state.count} times</button>
    `;
}

Versions

Current Tags

VersionDownloads (Last 7 Days)Tag
3.5.11latest

Version History

VersionDownloads (Last 7 Days)Published
3.5.11
3.5.014
3.4.02
3.3.10
3.3.01
3.2.11
3.2.00
3.1.00
3.0.10
3.0.00
2.6.00
2.5.00
2.4.00
2.3.30
2.3.20
2.3.10
2.3.00
2.2.10
2.2.00
2.1.20
2.1.10
2.1.00
2.0.30
2.0.20
2.0.10
2.0.00
1.1.30
1.1.20
1.1.10
1.1.00
1.0.41
1.0.30
1.0.20
1.0.10
1.0.00
1.0.0-test0

Package Sidebar

Install

npm i suunta

Weekly Downloads

14

Version

3.5.1

License

ISC

Unpacked Size

118 kB

Total Files

28

Last publish

Collaborators

  • mmatsu