Skip to content

stukennedy/htmxx

Repository files navigation

HTMXX v5

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.

Back to the future of web development

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.

todomvc htmx demo gif

How to install

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.

How to use

Express server

const { Htmxx } = require('htmxx');
const htmxx = new Htmxx(process.cwd());
htmxx.startServer();

Routes folder

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 a GET endpoint at /
  • src/routes/customers.post.ts defines a POST endpoint at /customers

Special files

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:

Layout files

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.

Error files

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.

parameterised filenames

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.

component files or helpers

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.

Writing TS endpoint files

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.

Redirecting from an endpoint

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)

Websockets

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.

Bespoke Server (e.g. serverless)

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);

TODO & Twitter Example

See the example folder for a TodoMVC and Twitter Clone implementation in HTMXX running on ExpressJS.

Resources

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published