{"id":20863468,"url":"https://github.com/simplr/suunta","last_synced_at":"2025-05-12T09:32:07.518Z","repository":{"id":134792728,"uuid":"609143219","full_name":"Simplr/suunta","owner":"Simplr","description":"The new router","archived":false,"fork":false,"pushed_at":"2024-11-10T15:46:54.000Z","size":853,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-10T16:22:52.464Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Simplr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-03T13:19:18.000Z","updated_at":"2024-11-10T15:46:57.000Z","dependencies_parsed_at":"2023-07-05T13:05:06.616Z","dependency_job_id":"73e8fb51-6072-4748-9934-b3ae7767ae62","html_url":"https://github.com/Simplr/suunta","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Simplr%2Fsuunta","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Simplr%2Fsuunta/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Simplr%2Fsuunta/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Simplr%2Fsuunta/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Simplr","download_url":"https://codeload.github.com/Simplr/suunta/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225133080,"owners_count":17425934,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-18T05:29:09.970Z","updated_at":"2025-05-12T09:32:07.494Z","avatar_url":"https://github.com/Simplr.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Title Image](assets/suunta-banner.png)\n\n# Suunta\n\nA simple SPA routing and state management library for everyday use\n\n**Demo**\n\nFor an interactive demo, visit [ReplIt](https://replit.com/@huhtamatias/Suunta-Sandbox)\n\n## Table of Contents\n\n   * [Install](#install)\n   * [Usage](#usage)\n      + [Dynamic routes](#dynamic-routes)\n      + [State ](#state)\n         - [Global State](#global-state)\n      + [Requests](#requests)\n      + [Named routes](#named-routes)\n      + [Redirects](#redirects)\n         - [Not Found -pages](#not-found-pages)\n         - [Not Found -pages with redirect](#not-found-pages-with-redirect)\n      + [Dynamic imports](#dynamic-imports)\n      + [Rendering into outlets](#rendering-into-outlets)\n      + [Sub-views](#sub-views)\n      + [Hooks](#hooks)\n\n\n\n## Install\n\n```bash\nnpm install suunta\n```\n\n## Usage\n\nSuunta doesn't pack any dependencies, and therefore doesn't bring it's own rendering library either.\n\nThe easiest way to get started is to install [lit](https://lit.dev/) and create a renderer with that as shown below.\n\n```typescript\nimport { FooView } from \"./foo\";\nimport { html, render } from \"lit\";\nimport { Suunta } from \"suunta\";\n\nconst routes = [\n    {\n        path: \"/\",\n        view: html`\u003cp\u003eHello world!\u003c/p\u003e`\n    },\n    {\n        path: \"/foo\",\n        view: FooView,\n        title: \"Example - Foo View\"\n    }\n] as const;\n\n// This part can be written however you want. Suunta provides you with the \n// necessary data, you handle the rendering.\nconst renderer = (view, route, renderTarget) =\u003e {\n    render(html`${view}`, renderTarget);\n};\n\nconst routerOptions = {\n    routes,\n    renderer,\n    target: document.body\n};\n\nconst router = new Suunta(routerOptions);\n\nrouter.start();\n```\n\n\n\n### Dynamic routes\n\nSuunta supports dynamic routes with the `{keyword}`-notation. \nIf you want the matching to only match certain types of data, you can supply a regex for the matcher.\n\nYou can access properties of your dynamic routes with `router.getCurrentView()?.route.params?.id`\n\n```typescript\nconst routes: Route[] = [\n    {\n        path: \"/user/{id}(\\\\d+)\",\n        name: \"User profile\",\n        view: () =\u003e html`\u003cp\u003eUser page for id ${router?.getCurrentView()?.params.id}\u003c/p\u003e`\n    },\n    {\n        path: \"/search/{matchAll}\",\n        name: \"Search\",\n        view: html`\u003cp\u003eSearch page for ${router?.getCurrentView()?.params.matchAll}\u003c/p\u003e`\n    },\n    {\n        path: \"/user/{id}(\\\\d+)/search/{matchAll}\",\n        name: \"User profile with search\",\n        view: () =\u003e html`\n            \u003cp\u003eUser page for id ${router?.getCurrentView()?.params.id}\u003c/p\u003e\n            \u003cp\u003eSearch page for ${router?.getCurrentView()?.params.matchAll || \"Nothing\"}\u003c/p\u003e\n        `\n    },\n    {\n        path: \"/{notFoundPath}(.*)\",\n        name: \"404\",\n        view: html`\u003cp\u003ePage not found\u003c/p\u003e`\n    },\n];\n```\n\n### State \n\nA lot of views have state. And that state can change, and so should the page content with it.\n\nFor state management, Suunta provides a `createState` hook, which will take the initial state of your view as a parameter.\n\nWhen any of the values of that state object is directly manipulated, the view will update accordingly.\n\n```typescript\nimport { html } from 'lit';\nimport { createState } from 'suunta/state';\n\nexport const View = () =\u003e {\n    const state = createState({\n        count: 0,\n    });\n\n    const addCount = () =\u003e {\n        state.count += 1;\n    };\n\n    return () =\u003e html`\n        \u003cp\u003eFoo View\u003c/p\u003e\n        \u003cp\u003eCount: ${state.count}\u003c/p\u003e\n        \u003cbutton @click=${addCount}\u003eCount++\u003c/button\u003e\n    `;\n};\n```\n\n#### Global State\n\nFor some use cases you might want to have state that is shared between multiple views, and is also reactive.\n\nFor these cases, the use of the `createGlobalState` hook is recommended.\n\n**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\nproductivity and efficiency of the application.**\n\nWhen values of globalState objects are updated, all of the views managed by the current Suunta instance will be updated.\n\nFor most applications only using a single view at a time, this won't affect performance, but for views with \nsubviews through the Child Routes, this will cause an performance hit.\n\n```javascript\n// ../index.js\nimport { createGlobalState } from \"suunta\";\n\nexport const globalState = createGlobalState({\n    count: 0\n})\n\n\n// FooView.js\nimport { html } from 'lit';\nimport { createState } from 'suunta/state';\nimport { globalState } from \"../index.js\";\n\nexport const View = () =\u003e {\n    const addCount = () =\u003e {\n        globalState.count += 1;\n    };\n\n    return () =\u003e html`\n        \u003cp\u003eFoo View\u003c/p\u003e\n        \u003cp\u003eCount: ${globalState.count}\u003c/p\u003e\n        \u003cbutton @click=${addCount}\u003eCount++\u003c/button\u003e\n    `;\n};\n```\n\n### Requests\n\nMost of modern web applications tend to handle some kind of API calls to an external service.\n\nWhen managing async connections to external services, you need to manage multiple states. There's loading, errors, data etc.\n\nSuunta comes packed with an utility class inside the `suunta/fetch` sub-package, which provides request \nstate management that works with the Suunta state system.\n\n```typescript\nimport { html } from \"lit\";\nimport { fetchPending, pendingApiResponse } from \"suunta/fetch\";\n\nexport function View() {\n  const request = pendingApiResponse(\n    fetchPending\u003cGetAllCustomerInfoResponse\u003e(\"http://localhost:8080/customers\"),\n  );\n\n    return () =\u003e html`\n        \u003ch2\u003eUsers\u003c/h2\u003e\n\n        ${request.loading\n          ? html`\u003cp\u003eLoading...\u003c/p\u003e`\n          : html`\n              \u003cul\u003e\n                ${request.result.customers.map(\n                  (c) =\u003e html` \u003cli\u003e${c.firstName} ${c.lastName}\u003c/li\u003e `,\n                )}\n              \u003c/ul\u003e\n        `}\n    `;\n}\n```\n\nThe `pendingApiResponse` function works out of the box with [Hey API](https://heyapi.dev/) generated SDK's.\n\n```typescript\nimport { getAllCustomerInfo } from \"../hey-api/sdk.gen\";\n\nconst request = pendingApiResponse(getAllCustomerInfo);\n```\n\nThere is also a out-of-the-box implementation with Suunta named `fetchPending`, which only wraps the fetch API \nand provides some simple utilities to it.\n\nSome people however might want some more granular control over their process and want to write their own fetch wrappers.\nThat is also supported and encouraged by Suunta! A good starting point would be something along the lines of:\n\n```typescript\nimport { RequestResult } from \"suunta/fetch/core\";\n\nexport function fetchPending\u003cT\u003e(input: RequestInfo | URL, init?: RequestInit): () =\u003e Promise\u003cRequestResult\u003cT\u003e\u003e {\n  return async function () {\n    const request = new Request(input, init);\n    const response = await fetch(request);\n\n    if (!response.ok) {\n      const error = await response.text();\n      return {\n        response,\n        request,\n        error,\n        data: undefined,\n      };\n    }\n\n    const data = await response.json() as T;\n\n    return {\n      response,\n      request,\n      error: undefined,\n      data,\n    };\n  };\n}\n```\n\n### Named routes\n\nWith Suunta, you don't have to go through the hassle of going through your whole codebase with CTRL - F after changing a route.\n\nYou can define your routes using the `resolve` function and generate routes dynamically by the name of said route.\n \n```typescript\n\nconst routes = [\n    {\n        name: \"Home\",\n        path: \"/\",\n        view: HomeView\n    },\n    {\n        name: \"UserView\",\n        path: \"/users/{userId}\",\n        view: UserView\n        children: [\n            {\n                name: \"UserAttendances\",\n                path: \"/attendances/{attendanceId}\",\n                view: UserAttendanceView\n            }\n        ]\n    },\n]\n\nconst homeView = router.resolve(\"Home\");\n// \u003e homeView =\u003e /\n\n\nconst userView = router.resolve(\"UserView\", 123);\n// \u003e userView =\u003e /users/123\n\nconst attendanceView = router.resolve(\"UserAttendances\", 123, \"suunta-course\");\n// \u003e attendanceView =\u003e /users/123/attendances/suunta-course\n\nhtml`\u003ca href=\"${router.resolve(\"UserView\", 123)}\"\u003eTo user view\u003c/a\u003e`\n```\n\nIf you define your routes as a constant, you will also get Typescript type hints for your routes.\n\n```typescript\nconst routes = [\n    {\n        name: \"Home\",\n        path: \"/\",\n        view: HomeView\n    },\n    {\n        name: \"UserView\",\n        path: \"/users/{userId}\",\n        view: UserView\n        children: [\n            {\n                name: \"UserAttendances\",\n                path: \"/attendances/{attendanceId}\",\n                view: UserAttendanceView\n            }\n        ]\n    },\n] as const;\n\n//          _________________ \n//          |Home            |\n//          |UserView        |\n//          |UserAttendances |\n//          |________________|\nrouter.resolve(\"\")\n```\n\n### Redirects\n\nSupplying redirects is as easy as adding a `redirect` property onto your route, and targetting another view by `name` with it.\n\n```typescript\nconst routes: Route[] = [\n    {\n        path: \"/\",\n        name: \"Home\",\n        view: html`\u003cp id=\"needle\"\u003eHello world!\u003c/p\u003e`\n    },\n    {\n        path: \"/redirect\",\n        name: \"Redirect\",\n        redirect: \"Home\"\n    }\n]\n```\n\n#### Not Found -pages\n\nProviding 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.\n\n```typescript\nconst routes: Route[] = [\n    {\n        path: \"/\",\n        name: \"Home\",\n        view: html`\u003cp id=\"needle\"\u003eHello world!\u003c/p\u003e`\n    },\n    {\n        path: \"/{notFoundPath}(.*)\",\n        name: \"404\",\n        view: html`\u003cp\u003ePage not found\u003c/p\u003e`\n    },\n]\n```\n\n#### Not Found -pages with redirect\n\nYou can also make your 404 pages a redirect\n\n```typescript\nconst routes: Route[] = [\n    {\n        path: \"/\",\n        name: \"Home\",\n        view: html`\u003cp id=\"needle\"\u003eHello world!\u003c/p\u003e`\n    },\n    {\n        path: \"/{notFoundPath}(.*)\",\n        name: \"404\",\n        redirect: \"Home\"\n    },\n]\n```\n\n### Dynamic imports\n\nFor cases where you have a bunch of views and want to squeeze out some extra performance from your packages, \nyou can package split your code by dynamically importing your routes.\n\nSuunta will handle the rest.\n\n```typescript\n// ./views/foo.js\nimport { html } from \"lit\";\n\nexport const FooView = () =\u003e html`\u003cp id=\"needle\"\u003e\n    Foo bar\n\u003c/p\u003e`;\n\n// router.js\nimport { BarView } from \"./views/bar.js\";\n\nconst FooView = () =\u003e import(\"./views/foo.js\");\n\nconst routes: Route[] = [\n    {\n        path: \"/\",\n        name: \"Home\",\n        view: html`\u003cp id=\"needle\"\u003eHello world!\u003c/p\u003e`\n    },\n    {\n        path: \"/foo\",\n        name: \"Foo\",\n        view: FooView\n    },\n    {\n        path: \"/bar\",\n        name: \"Bar\",\n        view: BarView\n    },\n];\n\nconst routerOptions: SuuntaInitOptions = {\n    routes,\n    target: \"#outlet\"\n};\n\nrouter = new Suunta(routerOptions);\n```\n\n### Rendering into outlets\n\nBy using a `\u003csuunta-view\u003e` pseudoelement, you can tell Suunta to render the wanted content to a said location on page.\n\nBy default Suunta will render into the `document.body`\n\n```html\n\u003cbody\u003e\n    \u003csuunta-view\u003e\u003c/suunta-view\u003e\n\u003c/body\u003e\n```\n\n### Sub-views\n\nThe `\u003csuunta-view\u003e` 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,\nyou can do that utilizing the suunta-view element and `child routes`\n\n```typescript\nconst routes: Route[] = [\n    {\n        path: '/',\n        name: 'Home',\n        view: HelloView    \n    },\n    {\n        path: '/sub',\n        view: SubView,\n        children: [\n            {\n                path: '/sub',\n                view: SubView,\n                children: [\n                    {\n                        path: '/sub',\n                        view: SubView,\n                        children: [\n                            {\n                                path: '/sub',\n                                view: SubViewFloor,\n                            },\n                        ],\n                    },\n                ],\n            },\n        ],\n    },\n}\n\nexport function SubView() {\n    return () =\u003e html`\n        \u003cp\u003e\n            This is a view. By adding a child view to this view, and appending a\n            \u003ccode\u003e\u0026ltsuunta-view\u0026gt\u003c/code\u003e container into it, we can render subviews\n        \u003c/p\u003e\n\n        \u003ca href=\"${window.location.href}/sub\"\u003eDeeper\u003c/a\u003e\n\n        \u003csuunta-view\u003e\u003c/suunta-view\u003e\n    `;\n}\n```\n\nBy navigating to `/sub/sub/sub/sub`, we get a DOM looking like this:\n\n```html\n\u003cbody\u003e\n    \u003cp\u003eThis is a view...\u003c/p\u003e\n\n    \u003ca href=\"/sub/sub\"\u003eDeeper\u003c/a\u003e\n\n    \u003csuunta-view\u003e\n        \u003cp\u003eThis is a view...\u003c/p\u003e\n\n        \u003ca href=\"/sub/sub/sub\"\u003eDeeper\u003c/a\u003e\n\n        \u003csuunta-view\u003e\n            \u003cp\u003eThis is a view...\u003c/p\u003e\n\n            \u003ca href=\"/sub/sub/sub/sub\"\u003eDeeper\u003c/a\u003e\n\n            \u003csuunta-view\u003e\n                \u003cp\u003eThis is a subview floor\u003c/p\u003e\n            \u003c/suunta-view\u003e\n\n        \u003c/suunta-view\u003e\n\n    \u003c/suunta-view\u003e\n\u003c/body\u003e\n```\n\nAnd when navigating backwards, only the subviews are un-rendered. The whole page does not require a refresh.\n\n### Hooks\n\nSuunta provides lifecycle hooks to plug into the navigation phases from within your own views.\n\n```typescript\nimport { html } from \"lit-html\";\nimport { onNavigated, onUpdated } from \"suunta/triggers\";\nimport { createState } from \"suunta/state\";\n\nexport function HomeView() {\n\n    const state = createState({\n        count: 0\n    });\n\n    console.log(\"HomeView loaded\");\n    \n    // Triggers whenever a navigation has occured\n    onNavigated(() =\u003e {\n        console.log(\"HomeView navigated to\");\n    });\n\n    // Triggers whenever the current view's state object's value is updated\n    // e.g. when state.count is incremented\n    onUpdated(updatedProperties =\u003e {\n        console.log('Update', updatedProperties);\n    });\n\n    return () =\u003e html`\n        \u003cbutton @click=${() =\u003e state.count += 1}\u003eClicked ${state.count} times\u003c/button\u003e\n    `;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplr%2Fsuunta","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimplr%2Fsuunta","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplr%2Fsuunta/lists"}