Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ajanibilby/htmx-router
A remix.js style file path router for htmX websites
https://github.com/ajanibilby/htmx-router
Last synced: about 1 month ago
JSON representation
A remix.js style file path router for htmX websites
- Host: GitHub
- URL: https://github.com/ajanibilby/htmx-router
- Owner: AjaniBilby
- Created: 2023-07-27T07:27:32.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2023-08-15T06:56:34.000Z (over 1 year ago)
- Last Synced: 2024-11-21T12:23:09.165Z (about 2 months ago)
- Language: TypeScript
- Homepage:
- Size: 58.6 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
Awesome Lists containing this project
README
# htmX Router
> A [remix.js](https://remix.run/docs/en/main/guides/routing) style file path router for htmX websites
This library attempts to be as unopinionated as possible allowing for multiple escape hatches in-case there are certain times you want a different style of behaviour.
This library does not rely on any heavy weight dependencies such as react, instead opting to be built on the lighter weight [kitajs/html](https://kitajs.github.io/html/) library for it's JSX rendering, and using [csstype](https://www.npmjs.com/package/csstype), just as a type interface to improve developer ergonomics.
You can also see an example site running this library [here](https://github.com/AjaniBilby/predictable) with source code as an extra helpful example. Please be mindful the slow loading of this site is actually due to Discord APIs, and the rendering is taking less than 2ms on a raspberry pi on my floor.
- [htmX Router](#htmx-router)
- [Routes](#routes)
- [Module Layout](#module-layout)
- [Auth Function](#auth-function)
- [Render Function](#render-function)
- [CatchError Function](#catcherror-function)
- [Router](#router)
- [Types](#types)
- [RenderArgs](#renderargs)
- [Outlet](#outlet)
- [setTitle](#settitle)
- [addMeta](#addmeta)
- [addLinks](#addlinks)
- [renderHeadHTML](#renderheadhtml)
- [shared](#shared)
- [ErrorResponse](#errorresponse)
- [Override](#override)
- [Redirect](#redirect)
- [Components](#components)
- [Link](#link)
- [StyleCSS](#stylecss)## Routes
There are two requirements for this package behave correctly, you need a `root.jsx`/`.tsx`, in the same folder as a `routes` sub-folder. Plus any given route **must** have use the `route name` provided as the top element's `id` - which will be explained more in later.
URLs are resolved based on the file structure of yur `routes` folder, you can choose to use nested folders or `.`s to have all of your files in a single folder if you choose - all examples will use the `.` layout for simplicity.
If the url path `/user/1234/history` is requested, the router will create an outlet chain to your file tree (*this chain is actually just an array of modules, and the library works based on stack operations to reduce recursion and increase response times*).
```
root.tsx
routes
├── _index.tsx
├── user.tsx
├── user.static-path.tsx
└── user.$userID.tsx
```Given the file tree above, when the root function calls it's [Outlet](#outlet) function, that will trigger `user` to render, and when user calls it's [Outlet](#outlet) function, that will trigger `user.$userID.tsx` to render as this sub route didn't match with any of the static options available, so instead it matched with the wild card route.
Since there is no `user.$userID.history.tsx` file, if `user.$userID.tsx` calls [Outlet](#outlet) it will trigger a 404 error, which is actually generated by an internal hidden route placed at the end of an Outlet chain when it is not able to match the rest of a given URL.
If we request `/user`, the root route will render, and the `user.tsx` route will render, with `user.tsx`'s [Outlet](#outlet) function actually returning a blank string. If we instead want this request to throw an error we should add a `user._index.tsx` file which on render always throws a `ErrorResponse`.
### Module Layout
The router will look for three functions when reading a module, [Render](#render), [CatchError](#catcherror), and [Auth](#auth). Any combination of all or none of these functions are allowed, with the only exception being your `root.tsx` which must have a [Render](#render) and a [CatchError](#catcherror) function.
#### Auth Function
```ts
export async function Auth({shared}: RenderArgs) {
if (!shared.auth?.isAdmin) throw new ErrorResponse(401, 'Unauthorised', "Unauthorised Access");
return;
}
```This function is ran on all routes resolved by this file, and it's child routes - no matter if the route itself is masked or not rendered. This function must return nothing, and instead signals failure via throwing an error. With the successful case being nothing was thrown.
#### Render Function
```ts
export async function Render(routeName: string, {}: RenderArgs): Promise
```The render function should follow the above function signature, the routeName string must be consumed if you're rendering a valid output, with the top most HTML element having the id assigned to this value. This helps the router dynamically insert new routes when using the [Link](#link) component.
> **Note** that a render function may not always run for a given request, and may sometimes be ommited when the router determines the client already has this information in their DOM, and instead only response with the new data and where it should be inserted.
>
> For authorisation checks which must always run for a given URL, please use the [Auth](#auth) functionOptionally this function can also `throw`, in this case the thrown value will boil up until it hits a [CatchError](#catcherror), unless it throws certain types from this library such as `Redirect` or `Override` which will boil all the way to the top without triggering any [CatchError](#catcherror) functions.
This allows a given route to return an arbitrary response without being nested within it's parent routes.
#### CatchError Function
```ts
export async function CatchError(rn: string, {}: RenderArgs, e: ErrorResponse): Promise
```This function behaves almost identically to [Render](#render) in the way that it's results will be embed within it's parents unless an `Override` is thrown, and it takes a `routeName` which must be used in the root `HTML` element as the id. And it is given [RenderArgs](#renderargs).
However this function **must not** call the [Outlet](#outlet) function within the [RenderArgs](#renderargs), as that will attempt to consume a failed child route as it's result.
## Router
The router itself can be generated via two different ways through the CLI from this library, dynamically or statically.
```bash
npx htmx-router ./source/website --dynamic
```When the command is ran it will generate a router based on the directory provided which should contain your root file and routes folder. This command will generate a `router.ts` which we recommend you git ignore from your project.
- **Static**: The static build will read your directories and statically import all of your routes into itself, allowing for easy bundling with tools such as `esbuild`
- **Dynamic**: Will instead generate a file will on startup will read your directory every time, and dynamically import your routes, which will make it unsuitable for use with webpackers, but allows for quick revisions and working well with tools such as `nodemon`.Once your router is generated you can simply import it and use it like the example below:
```ts
const url = new URL(req.url || "/", "http://localhost");
const out = await Router.render(req, res, url);
if (out instanceof Redirect) {
res.statusCode = 302;
res.setHeader('Location', out.location);
return res.end();
} else if (out instanceof Override) {
res.end(out.data);
} else {
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end(""+out);
}
```The `Router.render` function may output three possible types [Redirect](#redirect), [Override](#override), or a simple string. These two non-string types are to allow the boil up of overrides within routes, and allows you do handle them specific to your server environment.
## Types
### RenderArgs
This class has been designed to work well with object unpacking for ease of use, typically it's used in a style like this for routes that only need information about the `req` and `res` objects.
```ts
export async function Render(rn: string, {req, res}: RenderArgs) {
return "Hello World";
}
```However it also includes a bunch of functions to help with higher order features.
#### Outlet
The outlet function will call the child route to render, this function is asynchronous and will always return a string, or else it will throw. If there is nothing left in the outlet chain, it will return an empty string.
#### setTitle
This function will update the title that will be generated by the [renderHeadHTML](#renderheadhtml) function, as well as the trigger value for the title updater when using the [Link](#link) component.
You should consider when you call this function in conjunction to your [Outlet](#outlet) function, because if you run setTitle, after the outlet has been called it will override the title set by the child.
```tsx
export async function Render(rn: string, {setTitle, Outlet}: RenderArgs) {
setTitle("Admin Panel");return
;
Admin Panel
{await Outlet()}
}
```#### addMeta
This function allows you to add meta tags which will be rendered by the [renderHeadHTML](#renderheadhtml) function.
```ts
addMeta([
{ property: "og:title", content: `${guild.name} - Predictions` },
{ property: "og:image", content: banner }
], true);
```If the second argument of this function is set to `true` this function will override any meta tags currently set, replacing them with the inputted tags instead.
#### addLinks
This function behaves identically to [addMeta](#addmeta) but instead designed for link tags.
#### renderHeadHTML
This renders out the set meta and link tags for use in the `root.tsx` module, it also includes an embed script for updating the title for dynamic loads from the [Link](#link) component.
#### shared
There is also a blank object attached to all [RenderArgs](#renderargs) for sharing information between routes.
This can be used for various purposes, but one example is to hold onto decoded cookie values so that each session doesn't need to recompute them if they already have.
Such an example would look like this
```ts
import type { IncomingMessage } from "node:http";
import * as cookie from "cookie";export function GetCookies(req: IncomingMessage, shared: any): Record {
if (shared.cookie) return shared.cookie;shared.cookies = cookie.parse(req.headers.cookie || "");
return shared.cookies;
}
``````ts
import type { GetCookies } from "../shared/cookie.ts";export function GetCookies(rn: string, {shared}: RenderArgs) {
const cookies = GetCookies(shared);
// do stuff....
}
```### ErrorResponse
This class is a way of HTTP-ifying error objects, and other error states, if an error is thrown by a [Render](#render) or an [Auth](#auth) function that isn't already wrapped by this type, the error will then become wrapped by this type.
```ts
export class ErrorResponse {
code : number;
status : string;
data : any;
}
```### Override
If a render function throws a value of this type, it will boil all the way up to the original render call allowing it to bypass any parent manipulation.
```ts
export class Override {
data : string | Buffer | Uint8Array;
}
```### Redirect
This type behaves identically to override by is intended for boiling up specifically http redirect responses.
```ts
export class Redirect {
location: string;
}
```## Components
### Link
```ts
export function Link(props: {
to: string,
target?: string,
style?: string
}, contents: string[])
```
```tsx
View Profile
```This component overrides a normal ``, adding extra headers telling the server route it is coming from, based on this information the server can determine the minimal route that needs to be rendered to send back to the client and calculates just that sub route.
Sending it back to the client telling htmX where to insert the new content.
This element will encode as a standard `` with some extra html attributes, meaning it won't affect SEO and bots attempting to scrape your website.
## StyleCSS
> **DEPRECATED**: If you utilize `@kitajs/html` instead of `typed-html` this function is no longer needed
This is a helper function allowing you to give it a [CSS.Properties](https://www.npmjs.com/package/csstype) type, and render it into a string for [kitajs/html](https://kitajs.github.io/html/) to use.
```tsx
I AM BIG
```