Express </> HTMX Components

Component API

component.init( )

component.init(app, componentsDir, options);

Returns a Promise.

Recursively loads components.

Arguments:

  • app = Express application instance
  • componentsDir = directory to scan for components
  • options = options object:
    • js = array of additional javascript files to include
    • css = array of css files to include
    • htmx = htmx library to include. Defaults to "https://unpkg.com/htmx.org@1.9.9"
    • favicon = link to a favicon
    • link = add link tags to the document head

Example:

const express = require('express');
const component = require('express-htmx-components');

const app = express();

component.init(app,'./components')
    .then(() => app.listen(8888))
    .catch(err => console.error(err));

Adding global js and css files to your app:

component.init(app, "./components", {
  js: [
    "https://unpkg.com/htmx.org/dist/ext/json-enc.js",
    "https://unpkg.com/htmx.org/dist/ext/alpine-morph.js",
  ],
  css: [
    "https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css",
  ],
});

Overriding default htmx library to include:

component.init(app, "./components", {
  htmx: "/static/js/htmx.js",
});

// or

component.init(app, "./components", {
  htmx: {
    src: "https://unpkg.com/htmx.org@1.9.9/dist/htmx.min.js",
    integrity: "sha384-QFjmbokDn2DjBjq+fM+8LUIVrAgqcNW2s0PjAxHETgRn9l4fvX31ZxDxvwQnyMOX",
    crossorigin: "anonymous",
  }
});

Add a favicon:

component.init(app, "./components", {
  favicon: "/static/icon.png",
});

// or

component.init(app, "./components", {
  favicon: {
    href: "/static/icon.png",
    type: "image/png",
  }
});

Add a manifest:

component.init(app, "./components", {
  link: [
    {
      rel: 'manifest'
      href: "/static/manifest.json",
    }
  ]
});

component.get( )

component.get(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component.

Arguments:

  • path = URL path for the component
  • middleware = optional, zero or more Express or Connect middlewares
  • componentDefinition = function defining the component (may be async)

The componentDefinition is just a function that returns an HTML string.

A single object will be passed to it when called containing all the props for the component. Query params, request bodies and path params are all automatically converted to props,

Example:

const component = require("express-htmx-components");

const getHello = component.get("/hello", () => "<h1>Hello World</h1>");

module.exports = {
  getHello,
};

Passing query params

const testing = component.get("/testing", ({ n }) => {
  return html`
    <h1>Number is: ${n}</h1>
  `;
});

Calling the component directly:

console.log(testing.html({ n: 100 }));

Or calling from the browser: http://localhost:8888/testing?n=100

Generates:

<h1>Number is: 100</h1>

Path parameter

To use a path parameter you need to pass a prop with the same name:

const testing = component.get("/testing/:n", ({ n }) => {
  return html`
    <h1>Number is: ${n}</h1>
  `;
});

This allows you to call it with: http://localhost:8888/testing/100

Accessing request session

A special session prop is passed into components which is linked to req.session:

const testing = component.get("/testing", ({ session }) => {
  return html`
    <h1>Hello ${session.user.name}</h1>
  `;
});

Redirects

To return a 302 redirect you can pass an additional parameter to your componentDefinition to access the redirect() function:

const testing = component.get("/testing", ({ session }, hx) => {
                                                     // ^ extra parameter
  if (!session.user) {
    return hx.redirect("/login");
  }

  return html`
    <h1>Hello ${session.user.name}</h1>
  `;
});

Accessing HTTP headers

The additional hx parameter also allows you to read the request headers and set the response headers using the hx.get() and hx.set() functions:

const testing = component.get("/testing", ({}, hx) => {
  hx.set("HX-Refresh", true); // set the HX-Refresh header

  // get user agent:
  return html`
    <div>User agent = ${hx.get("User-Agent")}</div>
  `;
});

component.post( )

component.post(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component. Behaves similar to component.get() but handles a POST request.

Example:

const component = require("express-htmx-components");

const postHello = component.post("/hello", () => "<h1>Hello World</h1>");

module.exports = {
  postHello,
};

Post body

Assuming you're using express.urlencoded() as the body parser, you can access post body the same way you access query params:

const testingGet = component.get("/testing", () => {
  return html`
    <div id="theNumber">
      <form hx-post="/testing" hx-target="#theNumber">
        <input type="text" name="n" />
        <button type="submit">Set Number</button>
      </form>
    </div>
  `;
});

const testingPost = component.post("/testing", ({ n }) => {
  return html` 
    <h1>Number is: ${n}</h1>
  `;
});

Accessing http://localhost:8888/testing will call the testingGet component but submitting the form will call the testingPost component.

File uploads

A files prop is passed to the component if you use a multipart/form-data middleware that can handle file uploads and set either the req.file or req.files property.

Currently, express-htmx-components support multer and express-fileupload.

Multer

To handle file uploads using multer's upload.single() or upload.array():

const myComponent = component.post("/test-single",
  upload.single('foo'),
  ({ files }) => {
    return html`
      <div>NAME = ${files[0].originalname}</div>
      <div>DATA = ${files[0].buffer.toString('utf8')}</div>
    `;
  }
);

const ourComponent = component.post("/test-multiple",
  upload.array('foo'),
  ({ files }) => {
    return html`
      $${files.map((f, idx) => {
        html`
          <div>
            <div>NAME${idx} = ${f.originalname}</div>
            <div>DATA${idx} = ${f.buffer.toString('utf8')}</div>
          </div>`;
      }).join('')}
    `;
  }
);

With both single() and array() express-htmx-components will pass the file or files into the files array.

Express-fileupload

To handle file uploads using express-fileupload:

const post = component.post("/test",
  fileUpload(),
  ({ files }) => {
    return html`
      <div>NAME = ${files.foo.name}</div>
      <div>DATA = ${files.foo.data.toString('utf8')}</div>
    `;
  }
);

Unlike multer, express-fileupload passes an object of files. The keys of the object are the form's input name (eg. <input type="file" name="foo">).

component.put( )

component.put(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component. Behaves similar to component.post() but handles a PUT request.

component.patch( )

component.patch(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component. Behaves similar to component.post() but handles a PATCH request.

component.del( )

component.patch(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component. Behaves similar to component.get() but handles a DELETE request.

component.use( )

component.use(path, ... middleware?, componentDefinition)

Returns a Component.

Defines a htmx component similar to the other component definition methods however this matches all request methods (get/post/put etc.).

Arguments:

  • path = URL path for the component
  • middleware = optional, zero or more Express or Connect middlewares
  • componentDefinition = function defining the component (may be async)

Request method

To figure out which method was used to call the component an additional property method is passed in as a prop:

const testing = component.use("/testing", ({ method, n }) => {
  if (method === "POST") {
    return html`<h1>${n}</h1>`;
  } else {
    return html`
      <form hx-post="/testing" hx-target="closest div">
        <input type="text" name="n" />
        <button type="submit">Submit</button>
      </form>
    `;
  }
});

Note that the method is passed in as UPPERCASE.

HTML Template Tags

To prevent XSS vulnerability and also for improve developer experience you should use HTML tagged template strings instead of plain template strings. When used in conjunction with VSCode plugins such as Inline HTML it provides HTML syntax support inside template literals.

html

The html tag escapes HTML special characters such as < to &lt; to prevent XSS attacks from user input.

const { html } = require("express-htmx-components/tags");

const data = '<script>alert("HA!")</script>';
console.log(html`Data is ${data}`);

Will output:

Data is &lt;script&gt;alert(&quot;HA!&quot;)&lt;/script&gt;

Since htmx components are just HTML strings the html tag allows you to insert raw HTML into the template string using a special $${} substitution:

const { html } = require("express-htmx-components/tags");

const data = "<h1>Hello</h1>";
console.log(html`
  ${data}
  $${data}
`);

Will output:

  &lt;h1&gt;Hello&lt;/h1&gt;
  <h1>Hello</h1>

css

The css tag is mainly for the improved developer experience with usng css syntax inside template literals. It does not do any additional processing apart from simply building the string as is:

const { css } = require("express-htmx-components/tags");

const style = css`
  #username {
    font-size: 14px;
    font-weight: bold;
  }
`;