tauri-axum-htmx
Version:
Build interactive UIs in Tauri applications using HTMX and Axum, enabling server-side rendering patterns by running the Axum app in the Tauri backend.
220 lines (188 loc) • 7.2 kB
JavaScript
const { invoke } = window.__TAURI__.core;
let localAppRequestCommand = "local_app_request";
export function initialize(initialPath, localAppRequestCommandOverride) {
if (localAppRequestCommandOverride) {
localAppRequestCommand = localAppRequestCommandOverride;
}
proxyFetch();
window.addEventListener("DOMContentLoaded", async () => {
const response = await window.fetch(initialPath);
document.documentElement.innerHTML = await response.text();
htmx.process(document.documentElement);
});
}
function proxyFetch() {
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const [url, options] = args;
if (url.startsWith("ipc://")) {
return originalFetch(...args);
}
const request = {
uri: url,
method: options?.method || "GET",
headers: options?.headers || {},
...(options?.body && { body: options.body }),
};
let response = await invoke(localAppRequestCommand, {
localRequest: request,
});
while ([301, 302, 303, 307, 308].includes(parseInt(response.status_code))) {
const location = response.headers["location"];
const redirectRequest = {
uri: location,
method: "GET",
headers: {},
};
response = await invoke("local_app_request", {
localRequest: redirectRequest,
});
}
const bodyByteArray = new Uint8Array(response.body);
const decoder = new TextDecoder("utf-8");
const bodyText = decoder.decode(bodyByteArray);
const status = parseInt(response.status_code);
const headers = new Headers(response.headers);
return new Response(bodyText, { status, headers });
};
}
// BEGIN XHR-FETCH-PROXY
(function (originalXMLHttpRequest) {
class EventTarget {
constructor() {
this.eventListeners = {};
}
addEventListener(event, callback) {
if (!this.eventListeners[event]) {
this.eventListeners[event] = [];
}
this.eventListeners[event].push(callback);
}
removeEventListener(event, callback) {
if (!this.eventListeners[event]) return;
const index = this.eventListeners[event].indexOf(callback);
if (index !== -1) {
this.eventListeners[event].splice(index, 1);
}
}
_triggerEvent(event, ...args) {
if (this.eventListeners[event]) {
this.eventListeners[event].forEach(callback => callback.apply(this, args));
}
}
}
class ProxyXMLHttpRequest extends EventTarget {
constructor() {
super();
this.onload = null;
this.onerror = null;
this.onreadystatechange = null;
this.readyState = 0;
this.status = 0;
this.statusText = '';
this.response = null;
this.res10ponseText = null;
this.responseType = '';
this.responseURL = '';
this.method = null;
this.url = null;
this.async = true;
this.requestHeaders = {};
this.controller = new AbortController(); // to handle aborts
this.eventListeners = {};
this.upload = new EventTarget(); // Adding upload event listeners
this._triggerEvent('readystatechange');
}
open(method, url, async = true, user = null, password = null) {
this.method = method;
this.url = url;
this.async = async;
this.user = user;
this.password = password;
this.readyState = 1;
this._triggerEvent('readystatechange');
}
send(data = null) {
const options = {
method: this.method,
headers: this.requestHeaders,
body: data,
signal: this.controller.signal,
mode: 'cors',
credentials: this.user || this.password ? 'include' : 'same-origin',
};
if (this.user && this.password) {
const base64Credentials = btoa(`${this.user}:${this.password}`);
options.headers['Authorization'] = `Basic ${base64Credentials}`;
}
this.readyState = 2;
this._triggerEvent('readystatechange');
fetch(this.url, options)
.then(response => {
this.status = response.status;
this.statusText = response.statusText;
this.responseURL = response.url;
this._parseHeaders(response.headers);
this.readyState = 3;
this._triggerEvent('readystatechange');
return this._parseResponse(response);
})
.then(responseData => {
this.readyState = 4;
this.response = responseData;
this.responseText = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
this._triggerEvent('readystatechange');
if (this.onload) this.onload();
})
.catch(error => {
if (this.onerror) this.onerror(error);
});
}
setRequestHeader(header, value) {
this.requestHeaders[header] = value;
}
abort() {
this.controller.abort();
this.readyState = 0;
this._triggerEvent('readystatechange');
}
getResponseHeader(header) {
return this.responseHeaders[header.toLowerCase()] || null;
}
getAllResponseHeaders() {
return Object.entries(this.responseHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\r\n');
}
overrideMimeType(mime) {
this.overrideMime = mime;
}
_parseHeaders(headers) {
this.responseHeaders = {};
headers.forEach((value, key) => {
this.responseHeaders[key.toLowerCase()] = value;
});
}
_parseResponse(response) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json();
} else if (contentType && (contentType.includes('text/') || contentType.includes('xml'))) {
return response.text();
} else {
return response.blob(); // default to blob for binary data
}
}
_triggerEvent(event, ...args) {
super._triggerEvent(event, ...args);
if (this[`on${event}`]) {
this[`on${event}`].apply(this, args);
}
if (event.startsWith('progress') || event === 'loadstart' || event === 'loadend' || event === 'abort') {
this.upload._triggerEvent(event, ...args);
}
}
}
window.XMLHttpRequest = ProxyXMLHttpRequest;
})(window.XMLHttpRequest);
// END XHR-FETCH-PROXY