folder restructuring, lifecycle stuf, upgrade to htmx 2

This commit is contained in:
maddalax 2024-09-12 20:31:18 -05:00
parent d84c8b5552
commit f50ded8589
28 changed files with 889 additions and 97 deletions

3
assets/css/input.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,14 @@
const {join} = require("node:path");
/** @type {import('tailwindcss').Config} */
const root = join(__dirname, "../../");
const content = join(root, "**/*.go");
console.log(content)
module.exports = {
content: [content],
theme: {
extend: {},
},
plugins: [],
};

View file

@ -1,11 +1,12 @@
import * as htmx from "htmx.org"; import htmx from "htmx.org";
htmx.defineExtension("debug", { htmx.defineExtension("debug", {
// @ts-ignore
onEvent: function (name, evt) { onEvent: function (name, evt) {
if (console.debug) { if (console.debug) {
console.debug(name, evt.target, evt); console.debug(name);
} else if (console) { } else if (console) {
console.log("DEBUG:", name, evt.target, evt); console.log("DEBUG:", name);
} else { } else {
// noop // noop
} }

View file

@ -0,0 +1,21 @@
import htmx from "htmx.org";
htmx.defineExtension("mutation-error", {
// @ts-ignore
onEvent: (name, evt) => {
if (!(evt instanceof CustomEvent)) {
return false;
}
if (name === "htmx:afterRequest") {
if (!evt.detail || !evt.detail.xhr) {
return;
}
const status = evt.detail.xhr.status;
if (status >= 400) {
htmx.findAll("[hx-on\\:\\:mutation-error]").forEach((element) => {
htmx.trigger(element, "htmx:mutation-error", { status });
});
}
}
},
});

View file

@ -1,4 +1,4 @@
import * as htmx from "htmx.org"; import htmx from "htmx.org";
function dependsOn(pathSpec: any, url: string) { function dependsOn(pathSpec: any, url: string) {
if (pathSpec === "ignore") { if (pathSpec === "ignore") {
@ -33,7 +33,11 @@ function refreshPath(path: string) {
} }
htmx.defineExtension("path-deps", { htmx.defineExtension("path-deps", {
// @ts-ignore
onEvent: function (name, evt) { onEvent: function (name, evt) {
if (!(evt instanceof CustomEvent)) {
return false;
}
if (name === "htmx:beforeOnLoad") { if (name === "htmx:beforeOnLoad") {
const config = evt.detail.requestConfig; const config = evt.detail.requestConfig;
// mutating call // mutating call

View file

@ -0,0 +1,136 @@
import htmx from "htmx.org";
const config: any = htmx.config;
/** @type {import("../htmx").HtmxInternalApi} */
let api: any;
const attrPrefix = "hx-target-";
// IE11 doesn't support string.startsWith
function startsWith(str: string, prefix: string) {
return str.substring(0, prefix.length) === prefix;
}
/**
* @param {HTMLElement} elt
* @param respCodeNumber
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt: Element, respCodeNumber: number) {
if (!elt || !respCodeNumber) return null;
const respCode = respCodeNumber.toString();
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
const attrPossibilities = [
respCode,
respCode.substr(0, 2) + "*",
respCode.substr(0, 2) + "x",
respCode.substr(0, 1) + "*",
respCode.substr(0, 1) + "x",
respCode.substr(0, 1) + "**",
respCode.substr(0, 1) + "xx",
"*",
"x",
"***",
"xxx",
];
if (startsWith(respCode, "4") || startsWith(respCode, "5")) {
attrPossibilities.push("error");
}
for (let i = 0; i < attrPossibilities.length; i++) {
const attr = attrPrefix + attrPossibilities[i];
const attrValue = api.getClosestAttributeValue(elt, attr);
if (attrValue) {
if (attrValue === "this") {
return api.findThisElement(elt, attr);
} else {
return api.querySelectorExt(elt, attrValue);
}
}
}
return null;
}
/** @param {Event} evt */
function handleErrorFlag(evt: CustomEvent) {
if (evt.detail.isError) {
if (config.responseTargetUnsetsError) {
evt.detail.isError = false;
}
} else if (config.responseTargetSetsError) {
evt.detail.isError = true;
}
}
htmx.defineExtension("response-targets", {
// @ts-ignore
init: (apiRef) => {
api = apiRef;
if (config.responseTargetUnsetsError === undefined) {
config.responseTargetUnsetsError = true;
}
if (config.responseTargetSetsError === undefined) {
config.responseTargetSetsError = false;
}
if (config.responseTargetPrefersExisting === undefined) {
config.responseTargetPrefersExisting = false;
}
if (config.responseTargetPrefersRetargetHeader === undefined) {
config.responseTargetPrefersRetargetHeader = true;
}
},
// @ts-ignore
onEvent: (name, evt) => {
if (!(evt instanceof CustomEvent)) {
return false;
}
if (
name === "htmx:beforeSwap" &&
evt.detail.xhr &&
evt.detail.xhr.status !== 200
) {
if (evt.detail.target) {
if (config.responseTargetPrefersExisting) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
if (
config.responseTargetPrefersRetargetHeader &&
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)
) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
}
if (!evt.detail.requestConfig) {
return true;
}
const target = getRespCodeTarget(
evt.detail.requestConfig.elt,
evt.detail.xhr.status,
);
if (target) {
handleErrorFlag(evt);
evt.detail.shouldSwap = true;
evt.detail.target = target;
}
return true;
}
},
});

View file

@ -0,0 +1,43 @@
import htmx, { HtmxSettleInfo, HtmxSwapStyle } from "htmx.org";
htmx.defineExtension("trigger-children", {
onEvent: (name, evt: Event | CustomEvent) => {
if (!(evt instanceof CustomEvent)) {
return false;
}
const target = evt.detail.target as HTMLElement;
if (target && target.children) {
Array.from(target.children).forEach((e) => {
htmx.trigger(e, name, null);
});
}
return true;
},
init: function (api: any): void {},
transformResponse: function (
text: string,
xhr: XMLHttpRequest,
elt: Element,
): string {
return text;
},
isInlineSwap: function (swapStyle: HtmxSwapStyle): boolean {
return false;
},
handleSwap: function (
swapStyle: HtmxSwapStyle,
target: Node,
fragment: Node,
settleInfo: HtmxSettleInfo,
): boolean | Node[] {
return false;
},
encodeParameters: function (
xhr: XMLHttpRequest,
parameters: FormData,
elt: Node,
) {},
getSelectors: function (): string[] | null {
return null;
},
});

View file

@ -1,18 +1,9 @@
import * as htmx from "htmx.org"; import htmx from "htmx.org";
import "./extensions/pathdeps"; import "./extensions/pathdeps";
import "./extensions/trigger-children"; import "./extensions/trigger-children";
import "./extensions/debug"; import "./extensions/debug";
import "./extensions/response-targets";
declare module "htmx.org" { import "./extensions/mutation-error";
// Example: Adding type definitions for an exported function
export function swap(
target: Element,
content: string,
spec?: {
swapStyle?: "innerHTML" | "outerHTML";
},
): any;
}
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) { function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
let lastUrl = window.location.href; let lastUrl = window.location.href;
@ -38,7 +29,11 @@ function onUrlChange(newUrl: string) {
} }
const split = triggers.split(", "); const split = triggers.split(", ");
if (split.find((s) => s === "url")) { if (split.find((s) => s === "url")) {
htmx.swap(element, "url"); htmx.swap(element, "url", {
swapStyle: "outerHTML",
swapDelay: 0,
settleDelay: 0,
});
} else { } else {
for (let [key, values] of url.searchParams) { for (let [key, values] of url.searchParams) {
let eventName = "qs:" + key; let eventName = "qs:" + key;
@ -60,6 +55,8 @@ function onUrlChange(newUrl: string) {
if (value) { if (value) {
htmx.swap(el, el.getAttribute(name) ?? "", { htmx.swap(el, el.getAttribute(name) ?? "", {
swapStyle: "innerHTML", swapStyle: "innerHTML",
swapDelay: 0,
settleDelay: 0,
}); });
hasMatch = true; hasMatch = true;
break; break;
@ -72,7 +69,7 @@ function onUrlChange(newUrl: string) {
htmx.swap( htmx.swap(
el, el,
el.getAttribute("hx-match-qp-mapping:" + defaultKey) ?? "", el.getAttribute("hx-match-qp-mapping:" + defaultKey) ?? "",
{ swapStyle: "innerHTML" }, { swapStyle: "innerHTML", swapDelay: 0, settleDelay: 0 },
); );
} }
} }

View file

@ -9,16 +9,29 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"htmx.org": "^1.9.12" "htmx.org": "~2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.7.26", "@swc/core": "^1.7.26",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tailwindcss": "^3.4.11",
"tsup": "^8.2.4", "tsup": "^8.2.4",
"typescript": "^5.6.2" "typescript": "^5.6.2"
} }
}, },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1", "version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
@ -992,6 +1005,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/array-union": { "node_modules/array-union": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@ -1064,6 +1083,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1138,6 +1166,18 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -1155,6 +1195,12 @@
} }
} }
}, },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1167,6 +1213,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -1320,6 +1372,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -1384,10 +1445,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "1.9.12", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.2.tgz",
"integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==" "integrity": "sha512-eUPIpQaWKKstX393XNCRCMJTrqPzikh36Y9RceqsUZLTtlFjFaVDgwZLUsrFk8J2uzZxkkfiy0TE359j2eN6hA=="
}, },
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
@ -1419,6 +1492,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -1491,6 +1579,15 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"node_modules/jiti": {
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
},
"node_modules/joycon": { "node_modules/joycon": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@ -1617,6 +1714,24 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -1647,6 +1762,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@ -1677,6 +1801,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-scurry": { "node_modules/path-scurry": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@ -1720,6 +1850,15 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pirates": { "node_modules/pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@ -1729,6 +1868,70 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/postcss": {
"version": "8.4.45",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": { "node_modules/postcss-load-config": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
@ -1771,6 +1974,50 @@
} }
} }
}, },
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
@ -1815,6 +2062,15 @@
} }
] ]
}, },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -1827,6 +2083,23 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@ -1952,6 +2225,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -2079,6 +2361,123 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/tailwindcss/node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -2206,6 +2605,12 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true "dev": true
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@ -2328,6 +2733,18 @@
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
},
"node_modules/yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
} }
} }
} }

View file

@ -5,19 +5,21 @@
"scripts": { "scripts": {
"watch": "tsup ./mhtml.ts --watch --config ./tsup.config.ts", "watch": "tsup ./mhtml.ts --watch --config ./tsup.config.ts",
"build": "tsup ./mhtml.ts --minify --config ./tsup.config.ts", "build": "tsup ./mhtml.ts --minify --config ./tsup.config.ts",
"tailwind:watch": "npx tailwindcss -i ./input.css -o ./output.css --watch",
"pretty": "prettier --write ." "pretty": "prettier --write ."
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"htmx.org": "^1.9.12" "htmx.org": "~2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.7.26", "@swc/core": "^1.7.26",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.11",
"tsup": "^8.2.4", "tsup": "^8.2.4",
"typescript": "^5.6.2", "typescript": "^5.6.2"
"prettier": "^3.3.3"
} }
} }

View file

@ -10,6 +10,6 @@
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": ["./*.ts"], "include": ["./*.ts", "./*.js"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View file

@ -3,19 +3,21 @@ import { defineConfig } from "tsup";
export default defineConfig({ export default defineConfig({
format: ["esm"], format: ["esm"],
entry: ["./src/mhtml.ts"], entry: ["./src/mhtml.ts"],
outDir: "./dist", outDir: "./../dist",
dts: false, dts: false,
shims: true, shims: true,
skipNodeModulesBundle: true, skipNodeModulesBundle: true,
clean: true, clean: false,
target: "es6", target: "esnext",
treeshake: false,
sourcemap: true,
platform: "browser", platform: "browser",
outExtension: () => { outExtension: () => {
return { return {
js: ".js", js: ".js",
}; };
}, },
minify: true, minify: false,
bundle: true, bundle: true,
// https://github.com/egoist/tsup/issues/619 // https://github.com/egoist/tsup/issues/619
noExternal: [/(.*)/], noExternal: [/(.*)/],

View file

@ -0,0 +1,49 @@
package patient
import (
"errors"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"mhtml/database"
"time"
)
type Patient struct {
Name string
ReasonForVisit string
AppointmentDate time.Time
LocationName string
}
type Service struct {
ctx *fiber.Ctx
}
func NewService(ctx *fiber.Ctx) *Service {
return &Service{}
}
type CreatePatientRequest struct {
Name string
ReasonForVisit string
LocationName string
}
func (c *Service) Create(request CreatePatientRequest) error {
time.Sleep(time.Second)
database.HSet("patients", uuid.New().String(), Patient{
Name: request.Name,
ReasonForVisit: request.ReasonForVisit,
AppointmentDate: time.Now(),
LocationName: "New York",
})
return errors.New("error creating patient")
}
func (c *Service) List() ([]*Patient, error) {
patients, err := database.HList[Patient]("patients")
if err != nil {
return nil, err
}
return patients, nil
}

View file

@ -51,12 +51,8 @@ func NewPartial(root Renderable) *Partial {
} }
} }
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func GetPartialPath(partial func(ctx *fiber.Ctx) *Partial) string { func GetPartialPath(partial func(ctx *fiber.Ctx) *Partial) string {
return GetFunctionName(partial) return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
} }
func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string { func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string {

91
h/lifecycle.go Normal file
View file

@ -0,0 +1,91 @@
package h
import (
"fmt"
)
var HxBeforeRequest = "hx-on::before-request"
var HxAfterRequest = "hx-on::after-request"
var HxOnMutationError = "hx-on::mutation-error"
type LifeCycle struct {
beforeRequest []JsCommand
afterRequest []JsCommand
onMutationError []JsCommand
}
func NewLifeCycle() *LifeCycle {
return &LifeCycle{
beforeRequest: []JsCommand{},
afterRequest: []JsCommand{},
onMutationError: []JsCommand{},
}
}
func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle {
l.beforeRequest = append(l.beforeRequest, cmd...)
return l
}
func (l *LifeCycle) AfterRequest(cmd ...JsCommand) *LifeCycle {
l.afterRequest = append(l.afterRequest, cmd...)
return l
}
func (l *LifeCycle) OnMutationError(cmd ...JsCommand) *LifeCycle {
l.onMutationError = append(l.onMutationError, cmd...)
return l
}
func (l *LifeCycle) Render() *Node {
beforeRequest := ""
afterReqest := ""
onMutationError := ""
for _, command := range l.beforeRequest {
beforeRequest += fmt.Sprintf("%s;", command.Command)
}
for _, command := range l.afterRequest {
afterReqest += fmt.Sprintf("%s;", command.Command)
}
for _, command := range l.onMutationError {
onMutationError += fmt.Sprintf("%s;", command.Command)
}
return Children(
If(beforeRequest != "", Attribute(HxBeforeRequest, beforeRequest)),
If(afterReqest != "", Attribute(HxAfterRequest, afterReqest)),
If(onMutationError != "", Attribute(HxOnMutationError, onMutationError)),
).Render()
}
type JsCommand struct {
Command string
}
func SetText(text string) JsCommand {
return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)}
}
func AddAttribute(name, value string) JsCommand {
return JsCommand{Command: fmt.Sprintf("this.setAttribute('%s', '%s')", name, value)}
}
func RemoveAttribute(name string) JsCommand {
return JsCommand{Command: fmt.Sprintf("this.removeAttribute('%s')", name)}
}
func AddClass(class string) JsCommand {
return JsCommand{Command: fmt.Sprintf("this.classList.add('%s')", class)}
}
func RemoveClass(class string) JsCommand {
return JsCommand{Command: fmt.Sprintf("this.classList.remove('%s')", class)}
}
func Alert(text string) JsCommand {
return JsCommand{Command: fmt.Sprintf("alert('%s')", text)}
}
func EvalJs(js string) JsCommand {
return JsCommand{Command: js}
}

View file

@ -41,7 +41,13 @@ func (page Builder) renderNode(node *Node) {
flatChildren := make([]Renderable, 0) flatChildren := make([]Renderable, 0)
for _, child := range node.children { for _, child := range node.children {
if child == nil {
continue
}
c := child.Render() c := child.Render()
flatChildren = append(flatChildren, child) flatChildren = append(flatChildren, child)
if c.tag == FlagChildrenList { if c.tag == FlagChildrenList {
for _, gc := range c.children { for _, gc := range c.children {

View file

@ -276,6 +276,9 @@ func PushQsHeader(ctx *fiber.Ctx, key string, value string) *Headers {
} }
func NewHeaders(headers ...string) *Headers { func NewHeaders(headers ...string) *Headers {
if len(headers)%2 != 0 {
return &Headers{}
}
m := make(Headers) m := make(Headers)
for i := 0; i < len(headers); i++ { for i := 0; i < len(headers); i++ {
m[headers[i]] = headers[i+1] m[headers[i]] = headers[i+1]
@ -372,6 +375,10 @@ func BeforeRequestSetAttribute(key string, value string) Renderable {
return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`) return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`)
} }
func OnMutationErrorSetText(text string) Renderable {
return Attribute("hx-on::mutation-error", `this.innerText = '`+text+`'`)
}
func BeforeRequestSetText(text string) Renderable { func BeforeRequestSetText(text string) Renderable {
return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`) return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`)
} }

View file

@ -1,13 +0,0 @@
import * as htmx from "htmx.org";
import { HtmxEvent } from "htmx.org";
htmx.defineExtension("trigger-children", {
onEvent: (name: HtmxEvent, evt: CustomEvent) => {
const target = evt.detail.target as HTMLElement;
if (target && target.children) {
Array.from(target.children).forEach((e) => {
htmx.trigger(e, name, null);
});
}
},
});

View file

@ -9,4 +9,15 @@ watch-gen:
go run ./tooling/watch.go --command 'go run ./tooling/astgen' go run ./tooling/watch.go --command 'go run ./tooling/astgen'
watch-js: watch-js:
cd js && npm run build && npm run watch cd assets/js && npm run build && npm run watch
setup-tailwind-cli:
cd assets/css && curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64
cd assets/css && chmod +x tailwindcss-macos-arm64
cd assets/css && mv tailwindcss-macos-arm64 tailwindcss
watch-css:
cd assets/css && ./tailwindcss -i input.css -o ./../dist/main.css --watch
build-css:
cd assets/css && ./tailwindcss -i input.css -o ./../dist/main.css --minify

18
k6.js
View file

@ -1,18 +0,0 @@
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '1m', target: 100 }, // Ramp-up to 100 RPS over 1 minute
{ duration: '10m', target: 100 }, // Stay at 100 RPS for 10 minutes
{ duration: '1m', target: 0 }, // Ramp-down to 0 RPS
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
},
};
export default function () {
http.get('http://localhost:3000/patients');
sleep(1 / 100); // Make 100 requests per second
}

View file

@ -12,7 +12,8 @@ import (
func main() { func main() {
f := fiber.New() f := fiber.New()
f.Static("/js", "./js")
f.Static("/public", "./assets/dist")
f.Use(func(ctx *fiber.Ctx) error { f.Use(func(ctx *fiber.Ctx) error {
if ctx.Cookies("mhtml-session") != "" { if ctx.Cookies("mhtml-session") != "" {

View file

@ -8,7 +8,7 @@ import (
func RootPage(children ...h.Renderable) h.Renderable { func RootPage(children ...h.Renderable) h.Renderable {
return h.Html( return h.Html(
h.HxExtension("path-deps"), h.HxExtension("path-deps, response-targets, mutation-error"),
h.Head( h.Head(
h.Script("https://cdn.tailwindcss.com"), h.Script("https://cdn.tailwindcss.com"),
h.Script("/js/dist/mhtml.js"), h.Script("/js/dist/mhtml.js"),

View file

@ -3,9 +3,15 @@ package pages
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"mhtml/h" "mhtml/h"
"mhtml/pages/base"
) )
func IndexPage(c *fiber.Ctx) *h.Page { func IndexPage(c *fiber.Ctx) *h.Page {
return h.NewPage(base.RootPage(h.P("this is cool"))) return h.NewPage(h.Html(
h.HxExtension("path-deps, response-targets, mutation-error"),
h.Head(
h.Script("https://cdn.tailwindcss.com"),
h.Script("/js/dist/mhtml.js"),
),
h.Body(),
))
} }

View file

@ -4,11 +4,9 @@ import (
"fmt" "fmt"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"mhtml/h" "mhtml/h"
"time"
) )
func Test(ctx *fiber.Ctx) *h.Page { func Test(ctx *fiber.Ctx) *h.Page {
time.Sleep(time.Second * 1)
text := fmt.Sprintf("News ID: %s", ctx.Params("id")) text := fmt.Sprintf("News ID: %s", ctx.Params("id"))
return h.NewPage( return h.NewPage(
h.Div(h.Text(text)), h.Div(h.Text(text)),

View file

@ -2,11 +2,9 @@ package patient
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/google/uuid" "mhtml/features/patient"
"mhtml/database"
"mhtml/h" "mhtml/h"
"mhtml/partials/sheet" "mhtml/partials/sheet"
"time"
) )
func Create(ctx *fiber.Ctx) *h.Partial { func Create(ctx *fiber.Ctx) *h.Partial {
@ -14,13 +12,22 @@ func Create(ctx *fiber.Ctx) *h.Partial {
reason := ctx.FormValue("reason-for-visit") reason := ctx.FormValue("reason-for-visit")
location := ctx.FormValue("location-name") location := ctx.FormValue("location-name")
database.HSet("patients", uuid.New().String(), Patient{ err := patient.NewService(ctx).Create(patient.CreatePatientRequest{
Name: name, Name: name,
ReasonForVisit: reason, ReasonForVisit: reason,
AppointmentDate: time.Now(), LocationName: location,
LocationName: location,
}) })
if err != nil {
ctx.Status(500)
return h.NewPartialWithHeaders(h.NewHeaders(""),
h.Div(
h.Text("Error creating patient"),
h.Class("text-red-500"),
),
)
}
headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{ headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{
"HX-Trigger": "patient-added", "HX-Trigger": "patient-added",
}) })

View file

@ -2,23 +2,15 @@ package patient
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"mhtml/database" "mhtml/features/patient"
"mhtml/h" "mhtml/h"
"mhtml/partials/sheet" "mhtml/partials/sheet"
"mhtml/ui" "mhtml/ui"
"strings" "strings"
"time"
) )
type Patient struct {
Name string
ReasonForVisit string
AppointmentDate time.Time
LocationName string
}
func List(ctx *fiber.Ctx) *h.Partial { func List(ctx *fiber.Ctx) *h.Partial {
patients, err := database.HList[Patient]("patients") patients, err := patient.NewService(ctx).List()
if err != nil { if err != nil {
return h.NewPartial(h.Div( return h.NewPartial(h.Div(
@ -35,7 +27,6 @@ func List(ctx *fiber.Ctx) *h.Partial {
} }
return h.NewPartial(h.Div( return h.NewPartial(h.Div(
h.HxExtension("debug"),
h.Class("mt-8"), h.Class("mt-8"),
h.Id("patient-list"), h.Id("patient-list"),
h.List(patients, Row), h.List(patients, Row),
@ -92,7 +83,8 @@ func ValidateForm(ctx *fiber.Ctx) *h.Partial {
func addPatientForm() h.Renderable { func addPatientForm() h.Renderable {
return h.Form( return h.Form(
h.TriggerChildren(), h.HxExtension("debug, trigger-children"),
h.Attribute("hx-target-5*", "#submit-error"),
h.Post(h.GetPartialPath(Create)), h.Post(h.GetPartialPath(Create)),
h.Class("flex flex-col gap-2"), h.Class("flex flex-col gap-2"),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
@ -124,10 +116,14 @@ func addPatientForm() h.Renderable {
Class: "rounded p-2", Class: "rounded p-2",
Type: "submit", Type: "submit",
}), }),
h.Div(
h.Id("submit-error"),
h.Class("text-red-500"),
),
) )
} }
func Row(patient *Patient, index int) h.Renderable { func Row(patient *patient.Patient, index int) h.Renderable {
return h.Div( return h.Div(
h.Class("flex flex-col gap-2 rounded p-4", h.Ternary(index%2 == 0, "bg-red-100", "")), h.Class("flex flex-col gap-2 rounded p-4", h.Ternary(index%2 == 0, "bg-red-100", "")),
h.Pf("Name: %s", patient.Name), h.Pf("Name: %s", patient.Name),

View file

@ -29,6 +29,23 @@ func Button(props ButtonProps) h.Renderable {
text := h.Text(props.Text) text := h.Text(props.Text)
lifecycle := h.NewLifeCycle().
BeforeRequest(
h.AddAttribute("disabled", "true"),
h.SetText("Loading..."),
h.AddClass("bg-gray-400"),
).
AfterRequest(
h.RemoveAttribute("disabled"),
h.RemoveClass("bg-gray-400"),
h.SetText(props.Text),
).
OnMutationError(
h.SetText("failed"),
h.AddClass("bg-red-400"),
h.RemoveAttribute("disabled"),
)
button := h.Button( button := h.Button(
h.If(props.Id != "", h.Id(props.Id)), h.If(props.Id != "", h.Id(props.Id)),
h.If(props.Children != nil, h.Children(props.Children...)), h.If(props.Children != nil, h.Children(props.Children...)),
@ -36,10 +53,8 @@ func Button(props ButtonProps) h.Renderable {
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class), h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
h.If(props.Get != "", h.Get(props.Get)), h.If(props.Get != "", h.Get(props.Get)),
h.If(props.Target != "", h.Target(props.Target)), h.If(props.Target != "", h.Target(props.Target)),
//h.Attribute("hx-indicator", "#spinner"),
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")), h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
h.BeforeRequestSetText("Loading..."), lifecycle,
h.AfterRequestSetText(props.Text),
text, text,
) )