Files
timetracker/ts/elements/dropdown.ts
T
lukas 04552aa8f6 GameStatusSelector: custom element + typed contract (retire Alpine)
The Game status dropdown is now a <game-status-selector> light-DOM custom
element: the Python builder emits the tag + kebab attrs htpy-style, behavior
lives in ts/elements/{dropdown,game-status-selector}.ts wired by the native
connectedCallback, and GameStatusSelectorProps is the codegen'd contract. The
~70-line inline-Alpine f-string is gone.

Also fix SimpleTable to collect and re-attach the media of its row/header
nodes: it stringifies cells into the table markup, which silently dropped each
cell component's declared Media — so a <game-status-selector> in a cell never
got its <script> emitted. Now Page() emits it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:09:52 +02:00

51 lines
1.8 KiB
TypeScript

export interface DropdownConfig {
patchUrl: string;
bodyKey: string; // server field name, e.g. "status" or "device_id"
event: string; // dispatched on document.body after a successful PATCH
csrf: string;
numericValue?: boolean; // parse the option value as a number
}
// Wires a light-DOM value-selector dropdown that lives inside `host`.
// Markup hooks (rendered server-side): [data-toggle], [data-menu],
// [data-label], and one or more [data-option][data-value].
export function initDropdown(host: HTMLElement, config: DropdownConfig): void {
const toggle = host.querySelector<HTMLElement>("[data-toggle]");
const menu = host.querySelector<HTMLElement>("[data-menu]");
const label = host.querySelector<HTMLElement>("[data-label]");
if (!toggle || !menu || !label) return;
const close = () => {
menu.hidden = true;
};
toggle.addEventListener("click", (event) => {
event.stopPropagation();
menu.hidden = !menu.hidden;
});
document.addEventListener("click", (event) => {
if (!host.contains(event.target as Node)) close();
});
host.querySelectorAll<HTMLElement>("[data-option]").forEach((option) => {
option.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const raw = option.dataset.value ?? "";
label.innerHTML = option.innerHTML;
close();
const body: Record<string, unknown> = {
[config.bodyKey]: config.numericValue ? Number(raw) : raw,
};
window
.fetchWithHtmxTriggers(config.patchUrl, {
method: "PATCH",
headers: { "Content-Type": "application/json", "X-CSRFToken": config.csrf },
body: JSON.stringify(body),
})
.then(() => document.body.dispatchEvent(new CustomEvent(config.event)))
.catch(() => console.error("Failed to update", config.patchUrl));
});
});
}