I've been working with an app that's using htmx for more than 3 months now and I am really amazed how it simplified things with less scripting. I've also been experimenting with Tauri for a while and loved it for how flexible it is when it comes to choosing any frontend framework.
A lot of people have been asking and looking for solutions how to integrate htmx with Tauri, however, htmx requires a URL as it uses XMLHttpRequest internally which makes this a very interesting problem.
The solution I have in mind is creating "fake responses". With this, htmx thinks that it successfully reached the server and got a response. My attempt is similar to Python responses and respx which I used a lot for unit testing.
I published my solution in https://github.com/roniemartinez/tauri-plugin-htmx as an NPM library. The next section explains how it is done internally.
Patching XMLHttpRequest
Modifying XMLHttpRequest has been quite a challenge since a lot of properties are readonly
. Luckily, this can be modified with Object.defineProperty()
. Now, for the trick, we need to dispatch an event as XMLHttpRequest is asynchronous. I've tried to dispatch all events at first but noticed that "load" event is enough.
const {invoke} = window.__TAURI__.tauri;
const COMMAND_PREFIX = "command:";
const patchedSend = async function (params) {
// Make readonly properties writable
Object.defineProperty(this, "readyState", {writable: true})
Object.defineProperty(this, "status", {writable: true})
Object.defineProperty(this, "statusText", {writable: true})
Object.defineProperty(this, "response", {writable: true})
// Set response
const query = new URLSearchParams(params);
this.response = await invoke(this.command, Object.fromEntries(query));
this.readyState = XMLHttpRequest.DONE;
this.status = 200;
this.statusText = "OK";
// We only need load event to trigger a XHR response
this.dispatchEvent(new ProgressEvent("load"));
};
window.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener('htmx:beforeSend', (event) => {
const path = event.detail.requestConfig.path;
if (path.startsWith(COMMAND_PREFIX)) {
event.detail.xhr.command = path.slice(COMMAND_PREFIX.length);
event.detail.xhr.send = patchedSend;
}
});
});
Creating a Tauri app with htmx as the frontend
Start a Tauri project using create-tauri-app
and choosing "Vanilla" Javascript/Typescript as frontend.
$ cargo create-tauri-app
✔ Project name · tauri-htmx-ts
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · Vanilla
✔ Choose your UI flavor · TypeScript
Template created! To get started run:
cd tauri-htmx-ts
npm install
npm run tauri dev
Install HTMX
npm i htmx.org
Install tauri-plugin-htmx
npm i tauri-plugin-htmx
Modify index.html
To use htmx we need to modify the form inside index.html
. There are a lot of ways to do it but I've made the structure similar to how it was before. Feel free to try other approaches in htmx.
Instead of using a URL in hx-post
(or hx-get
), use the command name prefixed with command:
(e.g. command:greet
).
Modify main.ts or main.js
If you are using TypeScript as frontend, remove everything in main.ts
and replacing it with just htmx
and tauri-plugin-htmx
imports.
If you are using JavaScript as frontend, you need to copy the JS files from node_modules
.
cp node_modules/htmx.org/dist/htmx.min.js src
cp node_modules/tauri-plugin-htmx/tauri-plugin-htmx.js src
Importing will be a little different in Javascript.
Run your Tauri app
You can now start your Tauri app and verify.
cargo tauri dev
Conclusion
Patching XMLHttpRequest is only one of the possible solutions for htmx. There are other suggested approaches like starting a separate server or a custom URI but we need to wait until these has been tried. Hopefully, the Tauri devs makes this baked into the Tauri so we don't need to patch things anymore. Or a much better solution is that Javascript functions will be called from htmx (see https://github.com/bigskysoftware/htmx/issues/103)