cleanup script, start adding extensions
This commit is contained in:
parent
0dea110ebc
commit
ea600bc0fa
20 changed files with 2601 additions and 107 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,2 +1,6 @@
|
|||
# Project exclude paths
|
||||
/tmp/
|
||||
node_modules/
|
||||
dist/
|
||||
js/dist
|
||||
js/node_modules
|
||||
21
h/tag.go
21
h/tag.go
|
|
@ -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
3
js/.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
package-lock.json
|
||||
13
js/extensions/debug.ts
Normal file
13
js/extensions/debug.ts
Normal 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
51
js/extensions/pathdeps.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
13
js/extensions/triggerchildren.ts
Normal file
13
js/extensions/triggerchildren.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
80
js/mhtml.js
80
js/mhtml.js
|
|
@ -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
80
js/mhtml.ts
Normal 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
2333
js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
js/package.json
Normal file
23
js/package.json
Normal 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
15
js/tsconfig.json
Normal 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
23
js/tsup.config.ts
Normal 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,
|
||||
});
|
||||
3
justfile
3
justfile
|
|
@ -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
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
}),
|
||||
),
|
||||
),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}),
|
||||
})...,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue