cleanup script, start adding extensions

This commit is contained in:
maddalax 2024-09-12 12:15:17 -05:00
parent 0dea110ebc
commit ea600bc0fa
20 changed files with 2601 additions and 107 deletions

4
.gitignore vendored
View file

@ -1,2 +1,6 @@
# Project exclude paths
/tmp/
node_modules/
dist/
js/dist
js/node_modules

View file

@ -77,12 +77,20 @@ func Attributes(attrs map[string]string) Renderable {
}
}
func Boost() Renderable {
return Attribute("hx-boost", "true")
}
func Attribute(key string, value string) Renderable {
return Attributes(map[string]string{key: value})
}
func TriggerChildren() Renderable {
return Attribute("hx-trigger-children", "")
return HxExtension("trigger-children")
}
func HxExtension(value string) Renderable {
return Attribute("hx-ext", value)
}
func Disabled() Renderable {
@ -108,6 +116,7 @@ func CreateTriggers(triggers ...string) []string {
type ReloadParams struct {
Triggers []string
Target string
Children Renderable
}
func ViewOnLoad(partial func(ctx *fiber.Ctx) *Partial) Renderable {
@ -121,7 +130,7 @@ func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) Renderable
"hx-get": GetPartialPath(partial),
"hx-trigger": strings.Join(params.Triggers, ", "),
"hx-target": params.Target,
}))
}), params.Children)
}
func PartialWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string) Renderable {
@ -232,8 +241,8 @@ func Div(children ...Renderable) Renderable {
return Tag("div", children...)
}
func PushUrlHeader(url string) *Headers {
return NewHeaders("HX-Push-Url", url)
func ReplaceUrlHeader(url string) *Headers {
return NewHeaders("HX-Replace-Url", url)
}
func CombineHeaders(headers ...*Headers) *Headers {
@ -261,7 +270,7 @@ func PushQsHeader(ctx *fiber.Ctx, key string, value string) *Headers {
if err != nil {
return NewHeaders()
}
return NewHeaders("HX-Push-Url", SetQueryParams(parsed.Path, map[string]string{
return NewHeaders("HX-Replace-Url", SetQueryParams(parsed.Path, map[string]string{
key: value,
}))
}
@ -409,7 +418,7 @@ func AfterRequestSetHtml(children ...Renderable) Renderable {
return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
}
func Children(children []Renderable) Renderable {
func Children(children ...Renderable) Renderable {
return &Node{
tag: FlagChildrenList,
children: children,

3
js/.prettierignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
package-lock.json

13
js/extensions/debug.ts Normal file
View file

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

51
js/extensions/pathdeps.ts Normal file
View file

@ -0,0 +1,51 @@
import * as htmx from "htmx.org";
function dependsOn(pathSpec: any, url: string) {
if (pathSpec === "ignore") {
return false;
}
const dependencyPath = pathSpec.split("/");
const urlPath = url.split("/");
for (let i = 0; i < urlPath.length; i++) {
const dependencyElement = dependencyPath.shift();
const pathElement = urlPath[i];
if (dependencyElement !== pathElement && dependencyElement !== "*") {
return false;
}
if (
dependencyPath.length === 0 ||
(dependencyPath.length === 1 && dependencyPath[0] === "")
) {
return true;
}
}
return false;
}
function refreshPath(path: string) {
const eltsWithDeps = htmx.findAll("[path-deps]");
for (let i = 0; i < eltsWithDeps.length; i++) {
const elt = eltsWithDeps[i];
if (dependsOn(elt.getAttribute("path-deps"), path)) {
htmx.trigger(elt, "path-deps", null);
}
}
}
htmx.defineExtension("path-deps", {
onEvent: function (name, evt) {
if (name === "htmx:beforeOnLoad") {
const config = evt.detail.requestConfig;
// mutating call
if (
config &&
config.verb !== "get" &&
evt.target != null &&
evt.target instanceof Element &&
evt.target.getAttribute("path-deps") !== "ignore"
) {
refreshPath(config.path);
}
}
},
});

View file

@ -0,0 +1,13 @@
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

@ -1,80 +0,0 @@
window.onload = function () {
// htmx.logger = function(elt, event, data) {
// if(console) {
// console.log(elt, event, data);
// }
// }
// onUrlChange(window.location.href);
function triggerChildren(event) {
const target = event.detail.target
const type = event.type
if(target && target.children && target.hasAttribute('hx-trigger-children')) {
Array.from(target.children).forEach(function(element) {
htmx.trigger(element, type);
});
}
}
const events = ['htmx:beforeRequest', 'htmx:afterRequest', 'htmx:responseError', 'htmx:sendError',
'htmx:timeout', 'htmx:xhr:abort',
'htmx:xhr:loadstart', 'htmx:xhr:loadend', 'htmx:xhr:progress']
events.forEach(function(event) {
document.addEventListener(event, triggerChildren)
})
window.history.pushState = new Proxy(window.history.pushState, {
apply: (target, thisArg, argArray) => {
if(argArray.length > 2) {
onUrlChange(window.location.origin + argArray[2]);
}
return target.apply(thisArg, argArray);
},
});
}
function onUrlChange(newUrl) {
let url = new URL(newUrl);
setTimeout(() => {
document.querySelectorAll('[hx-trigger]').forEach(function(element) {
const triggers = element.getAttribute('hx-trigger');
const split = triggers.split(", ");
console.log(split)
if(split.find(s => s === 'url')) {
htmx.trigger(element, "url");
} else {
for (let [key, values] of url.searchParams) {
let eventName = "qs:" + key
if (triggers.includes(eventName)) {
htmx.trigger(element, eventName);
break
}
}
}
});
}, 50)
document.querySelectorAll('[hx-match-qp]').forEach((el) => {
let hasMatch = false;
for (let name of el.getAttributeNames()) {
if(name.startsWith("hx-match-qp-mapping:")) {
let match = name.replace("hx-match-qp-mapping:", "");
let value = url.searchParams.get(match);
if(value) {
htmx.swap(el, el.getAttribute(name), {swapStyle: 'innerHTML'})
hasMatch = true;
break
}
}
}
if(!hasMatch) {
let defaultKey = el.getAttribute("hx-match-qp-default")
if(defaultKey) {
htmx.swap(el, el.getAttribute("hx-match-qp-mapping:" + defaultKey), {swapStyle: 'innerHTML'})
}
}
})
}

80
js/mhtml.ts Normal file
View file

@ -0,0 +1,80 @@
import * as htmx from "htmx.org";
import "./extensions/pathdeps";
import "./extensions/triggerchildren";
import "./extensions/debug";
declare module "htmx.org" {
// 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) {
let lastUrl = window.location.href;
setInterval(() => {
if (window.location.href !== lastUrl) {
callback(lastUrl, window.location.href);
lastUrl = window.location.href;
}
}, 100);
}
watchUrl((_, newUrl) => {
onUrlChange(newUrl);
});
function onUrlChange(newUrl: string) {
let url = new URL(newUrl);
document.querySelectorAll("[hx-trigger]").forEach(function (element) {
const triggers = element.getAttribute("hx-trigger");
if (!triggers) {
return;
}
const split = triggers.split(", ");
if (split.find((s) => s === "url")) {
htmx.swap(element, "url");
} else {
for (let [key, values] of url.searchParams) {
let eventName = "qs:" + key;
if (triggers.includes(eventName)) {
console.log("triggering", eventName);
htmx.trigger(element, eventName, null);
break;
}
}
}
});
document.querySelectorAll("[hx-match-qp]").forEach((el) => {
let hasMatch = false;
for (let name of el.getAttributeNames()) {
if (name.startsWith("hx-match-qp-mapping:")) {
let match = name.replace("hx-match-qp-mapping:", "");
let value = url.searchParams.get(match);
if (value) {
htmx.swap(el, el.getAttribute(name) ?? "", {
swapStyle: "innerHTML",
});
hasMatch = true;
break;
}
}
}
if (!hasMatch) {
let defaultKey = el.getAttribute("hx-match-qp-default");
if (defaultKey) {
htmx.swap(
el,
el.getAttribute("hx-match-qp-mapping:" + defaultKey) ?? "",
{ swapStyle: "innerHTML" },
);
}
}
});
}

2333
js/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

23
js/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "mhtml-js",
"version": "1.0.0",
"main": "mhtml.js",
"scripts": {
"watch": "tsup ./mhtml.ts --watch --config ./tsup.config.ts",
"build": "tsup ./mhtml.ts --minify --config ./tsup.config.ts",
"pretty": "prettier --write ."
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"htmx.org": "^1.9.12"
},
"devDependencies": {
"@swc/core": "^1.7.26",
"@types/node": "^22.5.4",
"tsup": "^8.2.4",
"typescript": "^5.6.2",
"prettier": "^3.3.3"
}
}

15
js/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES6",
"module": "es6",
"lib": ["es6", "dom"],
"moduleResolution": "node",
"noEmit": true,
"downlevelIteration": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true
},
"include": ["./*.ts"],
"exclude": ["node_modules"]
}

23
js/tsup.config.ts Normal file
View file

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

View file

@ -7,3 +7,6 @@ run-gen:
watch-gen:
go run ./tooling/watch.go --command 'go run ./tooling/astgen'
watch-js:
cd js && npm run build && npm run watch

View file

@ -8,10 +8,10 @@ import (
func RootPage(children ...h.Renderable) h.Renderable {
return h.Html(
h.HxExtension("path-deps"),
h.Head(
h.Script("https://cdn.tailwindcss.com"),
h.Script("https://unpkg.com/htmx.org@2.0.2"),
h.Script("/js/mhtml.js"),
h.Script("/js/dist/mhtml.js"),
),
h.Body(
partials.NavBar(),

View file

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

View file

@ -5,7 +5,6 @@ import (
"mhtml/h"
"mhtml/pages/base"
"mhtml/partials/patient"
"mhtml/partials/sheet"
)
func PatientsIndex(ctx *fiber.Ctx) *h.Page {
@ -18,13 +17,10 @@ func PatientsIndex(ctx *fiber.Ctx) *h.Page {
h.P("Manage Patients", h.Class("text-lg font-bold")),
patient.AddPatientButton(),
),
h.PartialWithTriggers(patient.List, "load", "patient-added from:body", "every 5s"),
h.If(
h.GetQueryParam(ctx, "adding") == "true",
h.View(patient.AddPatientSheetPartial, h.ReloadParams{
Triggers: h.CreateTriggers("load"),
Target: sheet.Id,
})),
h.View(patient.List, h.ReloadParams{
Triggers: h.CreateTriggers("load", "path-deps"),
Children: h.Children(h.Attribute("path-deps", h.GetPartialPath(patient.Create))),
}),
),
),
))

View file

@ -16,9 +16,10 @@ func NavBar() h.Renderable {
}
return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"),
h.Boost(),
h.Children(
h.Map(links, func(link Link) h.Renderable {
return h.A(link.Name, h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400"))
}),
})...,
))
}

View file

@ -34,6 +34,7 @@ func List(ctx *fiber.Ctx) *h.Partial {
}
return h.NewPartial(h.Div(
h.HxExtension("debug"),
h.Class("mt-8"),
h.Id("patient-list"),
h.List(patients, Row),
@ -41,9 +42,12 @@ func List(ctx *fiber.Ctx) *h.Partial {
}
func AddPatientSheetPartial(ctx *fiber.Ctx) *h.Partial {
closePathQs := h.GetQueryParam(ctx, "onClosePath")
return h.NewPartialWithHeaders(
h.PushQsHeader(ctx, "adding", "true"),
AddPatientSheet(h.CurrentPath(ctx)),
AddPatientSheet(
h.Ternary(closePathQs != "", closePathQs, h.CurrentPath(ctx)),
),
)
}
@ -107,7 +111,8 @@ func AddPatientButton() h.Renderable {
Id: "add-patient",
Text: "Add Patient",
Class: "bg-blue-700 text-white rounded p-2 h-12",
Trigger: "qs:adding, click",
Target: sheet.Id,
Get: h.GetPartialPath(AddPatientSheetPartial),
Get: h.GetPartialPathWithQs(AddPatientSheetPartial, "onClosePath=/patients"),
})
}

View file

@ -30,7 +30,7 @@ func Closed() h.Renderable {
func Close(ctx *fiber.Ctx) *h.Partial {
return h.NewPartialWithHeaders(
h.Ternary(ctx.Query("path") != "", h.PushUrlHeader(ctx.Query("path")), h.NewHeaders()),
h.Ternary(ctx.Query("path") != "", h.ReplaceUrlHeader(ctx.Query("path")), h.NewHeaders()),
h.Swap(ctx, Closed()),
)
}

View file

@ -31,7 +31,7 @@ func Button(props ButtonProps) h.Renderable {
button := h.Button(
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...)),
h.If(props.Trigger != "", h.Trigger(props.Trigger)),
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
h.If(props.Get != "", h.Get(props.Get)),