This is a total rewrite since v4 and not compatible. Going forward; HTMXX will be maintaining this Typescript approach instead of the now deprecated specialist html
files from pervious versions.
A zero config full-stack web framework in Typescript using HTMX and a routing structure. Reactive and responsive web development with proper REST endpoints (i.e. returning HTML not JSON) And no need for any client-side Javascript. Use Hyperscript for reactivity.
npm i htmxx
Then just point it at a the directory that contains your routes
folder.
This will contain your web-app file structure used to generate a resource of all the endpoints.
This resource can then be used by your server (e.g. Lambda function or ExpressJS) to render the endpoint.
const { Htmxx } = require('htmxx');
const htmxx = new Htmxx(process.cwd());
htmxx.startServer();
The web-app just consists of a routes folder containing the application routing structure as Typescript
files, that define the server endpoints.
e.g.
src/routes/index.ts
defines aGET
endpoint at/
src/routes/customers.post.ts
defines aPOST
endpoint at/customers
Any Typescript files will be treated as an endpoint unless they begin with an underscore _
(e.g. _layout.ts
). *.ts
is a GET
, other methods (POST
, PUT
, PATCH
, DELETE
) are supported in by adding the lowercase prefix to the Typescript extension e.g. *.post.ts
is a POST
.
Special files _layout.ts
and _error.ts
can be defined anywhere within our routes structure, these serve the following.
purposes:
These files allow nesting of content in your routes structure allowing layouts to be created at any level of the routing tree. They must contain a ${children}
instance inside the HTML template to tell the framework where the content is to be rendered. We add the import for HTMX in the root _layout.ts
file.
e.g.
import type { HtmxxRequest } from 'htmxx';
export default (request: HtmxxRequest, children: string) => {
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/[email protected]"></script>
<title>HTMXX Demo</title>
</head>
</head>
<body>
${children}
</body>
</html>
`;
};
If we navigate to /
, the index.ts
will be rendered in the body.
These files are displayed if an error occurs in the app.
An _error.ts
in the same directory as the error occured or in a directory above will be rendered in the case of an error.
In many routing frameworks you have the ability to define variable path names e.g. customers/:customerId
so that when a user navigates to customers/56
the code can retrieve the customerId
as 56 and load the correct data to display. This framework follows the same principle and it is defined by the folder and file structure.
If we create a folder structure customers/[customerId]
we can place an index.ts
in the customers
folder and another index.ts
in the [customerId]
folder, or we could have a file called [customerId].ts
in the customers
folder.
The <script server>
block in customers/[customerId].ts
or customers/[customerId]/index.ts
would have access to the params
variable which will contain customerId
in this case. To handle a POST
request for the specific customer you could use customers/[customerId]/index.post.ts
or customers/[customerId].post.ts
.
Any other Typescript file starting with an underscore _
will be excluded from the routing, but may be used in the app. This is helpful for creating re-usable components and fragments.
The Typescript endpoint files should export a default method that has (request: HtmxxRequest, children?: string)
as arguments and return a string containing the rendered HTML, usually as a string literal e.g.
// src/routes/index.ts
import type { HtmxxRequest } from 'htmxx';
import cust from './customers'; // some model data
export default async (request: HtmxxRequest) => {
const customers = await cust.getCustomers();
return /*html*/ `
<ul id="list">
${customers.map(({ name }) => `<li>${name}</li>`)}
</ul>
<div>
<input type="text" name="customerName" />
</div>
<button hx-post="/customer" hx-swap="innerHtml" hx-target="#list">
Add Name
</button>
`;
};
The HtmxxRequest
request contains the following attributes.
type HtmxxRequest = {
params: Record<string, string>;
query: Record<string, string>;
body: Record<string, string | number | undefined>;
headers: Record<string, string>;
redirect: (status: number, location: string) => void;
broadcast: (markup: string) => void;
};
(the children
argument is only used in layout files - see below)
e.g.
In some cases in your server-side code you may decide you need to redirect to a different route.
to do this, simply call redirect(statusCode, location)
in your script, the redirect method is available on the HtmxxRequest
.
e.g.
import type { HtmxxRequest } from 'htmxx';
export default async ({ redirect }: HtmxxRequest) => {
redirect(303, '/');
};
this will return { redirect: { status, location } }
from the callEndpoint
method. You can use status
and location
to do the redirect in your server. (the Express server included, already handles this automatically)
HTMX allows websocket support, so we have added it to our framework. In your HTMX files you can connect a form submission to a specific websocket endpoint like this
<div hx-ws="connect:/tweet">
<form hx-ws="send:submit">...</form>
</div>
To support this we provide an extra file type *.ws.ts
. In our example above we would define a websocket file like this:
// src/routes/tweet.ws.ts
import { HtmxxRequest } from 'htmxx';
import tweets from '~/model/tweets';
import Post from './_post';
export default ({ body }: HtmxxRequest) => {
const { message, username } = body;
const tweet = tweets.add(message, username);
return Post(tweet);
};
The body attribute contains the data sent by the form submission, so the code here is no different to writing a normal POST
. The markup that is rendered is now broadcast on the channel instead of returned in an HTTP response, but the HTMX is handled the same way by the form.
Other endpoints can choose to also broadcast their markup response as well as respond with it by calling the broadcast
method available on the HtmxxRequest
argument.
The following POST
endpoint generates a retweet fragment and broadcasts it to all clients.
// src/routes/retweet/[id].post.ts
import { HtmxxRequest } from 'htmxx';
import tweets from '~/model/tweets';
import Retweets from './_retweets';
export default ({ params }: HtmxxRequest) => {
const { id } = body;
const retweets = model.retweet(id);
return Retweets(id, retweets);
};
See the examples/twitterClone
folder for the full example.
When using a serverless solution, you will be provided with a function handler that is responding to endpoint requests. In this case express is not a good solution. So you can use the Htmxx processRoute
method directly without invoking the server. You will need to manage the response in the way your severless platform dictates.
// src/index.js
const { Htmxx } = require('htmxx');
// synchronously generate the list of all endpoints
const htmxx = new Htmxx('/src');
/* the full list of HtmxxFiles is now in htmxx.files */
console.log(htmxx.files);
// when a route is called with a specific method
const req: HtmxxRequest = {
body, // parsed request body
params, // parsed request params
query, // parsed queries
headers, // parsed header fields
redirect, // method to redirect the router
broadcast, // method to broadcast to websockets
};
const markup: string = await htmxx.processRoute(route, method, req);
See the example
folder for a TodoMVC and Twitter Clone implementation in HTMXX running on ExpressJS.