Skip to content

Instantly share code, notes, and snippets.

@acheong08
Created November 5, 2023 17:21
Show Gist options
  • Save acheong08/0c5cfe8276c1fba072c2556bdf4ffe60 to your computer and use it in GitHub Desktop.
Save acheong08/0c5cfe8276c1fba072c2556bdf4ffe60 to your computer and use it in GitHub Desktop.
HTMX but with JSON
class HTMJ {
constructor() {
this.init();
}
init() {
document.addEventListener("DOMContentLoaded", () => {
this.parseTemplates();
});
}
parseTemplates() {
const templates = document.querySelectorAll("template[hx-endpoint]");
templates.forEach((template) => {
const endpoint = template.getAttribute("hx-endpoint");
const dataSources = template.getAttribute("hx-data-sources");
let method = template.getAttribute("hx-method");
if (!method) {
method = dataSources ? "POST" : "GET";
}
const event = template.getAttribute("hx-event") || "onload";
const errorFuncName = template.getAttribute("hx-error");
let target = template.getAttribute("hx-target");
target = target ? document.querySelector(target) : template.parentNode;
// Check for a different event target
let eventTargetSelector = template.getAttribute("hx-event-target");
const eventTarget = eventTargetSelector
? document.querySelector(eventTargetSelector)
: target;
const fetchDataAndRender = async (e) => {
if (e) e.preventDefault(); // Prevent default form submission
const data = await this.fetchData(endpoint, method, dataSources);
if (data.error && errorFuncName) {
window[errorFuncName]?.(data.error);
} else if (data) {
this.renderTemplate(template, data, target);
}
};
if (event === "onload") {
fetchDataAndRender();
} else if (event === "onclick") {
eventTarget.addEventListener("click", fetchDataAndRender);
} else if (event === "onsubmit") {
eventTarget.addEventListener("submit", fetchDataAndRender);
}
});
}
async fetchData(endpoint, method, dataSources) {
try {
const dataToSend = this.prepareData(dataSources);
const options = {
method,
headers: { "Content-Type": "application/json" },
};
// Only attach body if method is not GET and dataSources are defined
if (method !== "GET" && dataToSend) {
options.body = JSON.stringify(dataToSend);
}
const response = await fetch(endpoint, options);
const data = await response.json();
if (!response.ok) {
return { error: data.error || "An error occurred" };
}
return data;
} catch (error) {
console.error("Failed to fetch data:", error);
return { error: "Failed to fetch data" };
}
}
prepareData(dataSources) {
if (!dataSources) return null;
const sources = dataSources.split(",").map((src) => src.trim());
const dataToSend = {};
sources.forEach((src) => {
const element = document.querySelector(src);
if (element) {
// Remove the '#' from the key
const key = src.startsWith("#") ? src.slice(1) : src;
dataToSend[key] = element.value || element.innerText;
}
});
return dataToSend;
}
renderTemplate(template, data, target) {
const fragment = document.createDocumentFragment();
// Helper function to replace placeholders and append clone to fragment
const renderData = (item) => {
const clone = document.importNode(template.content, true);
clone.querySelectorAll("*").forEach((node) => {
node.innerHTML = node.innerHTML.replace(
/\$\{([^}]+)\}/g,
(_, key) => item[key] || "",
);
});
fragment.appendChild(clone);
};
// Check if data is an array or object
if (Array.isArray(data)) {
data.forEach(renderData);
} else {
renderData(data);
}
const action = template.getAttribute("hx-action") || "update";
switch (action) {
case "append":
target.appendChild(fragment);
break;
case "swap":
target.replaceWith(fragment);
break;
case "update":
default:
target.innerHTML = "";
target.appendChild(fragment);
break;
}
}
}
new HTMJ();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment