a post-component library for building user-interfaces on the web.
1import type { Displayable, Renderable } from 'dhtml'
2import { assert, is_renderable } from '../shared.ts'
3import { type Cleanup } from './util.ts'
4
5export type Key = string | number | bigint | boolean | symbol | object | null
6
7export interface Controller {
8 _mount_callbacks?: Set<Cleanup> // undefined if mounted
9 _unmount_callbacks: Set<Cleanup>
10
11 _invalidate_queued?: Promise<void>
12 _invalidate?: () => void
13 _parent_node?: Node
14}
15
16const controllers: WeakMap<Renderable, Controller> = new WeakMap()
17
18export function get_controller(renderable: Renderable): Controller {
19 let controller = controllers.get(renderable)
20 if (controller) return controller
21
22 controller = {
23 _mount_callbacks: new Set(),
24 _unmount_callbacks: new Set(),
25 }
26
27 controllers.set(renderable, controller)
28 return controller
29}
30export function delete_controller(renderable: Renderable): void {
31 controllers.delete(renderable)
32}
33
34const keys: WeakMap<Displayable & object, Key> = new WeakMap()
35
36export function invalidate(renderable: Renderable): Promise<void> {
37 const controller = controllers.get(renderable)
38 assert(controller?._invalidate, 'the renderable has not been rendered')
39 return (controller._invalidate_queued ??= Promise.resolve().then(() => {
40 delete controller._invalidate_queued
41 controller._invalidate!()
42 }))
43}
44
45export function onMount(renderable: Renderable, callback: () => Cleanup): void {
46 assert(is_renderable(renderable), 'expected a renderable')
47
48 const controller = get_controller(renderable)
49 if (controller._mount_callbacks) {
50 controller._mount_callbacks.add(callback)
51 } else {
52 controller._unmount_callbacks.add(callback())
53 }
54}
55
56export function onUnmount(renderable: Renderable, callback: () => void): void {
57 onMount(renderable, () => callback)
58}
59
60export function getParentNode(renderable: Renderable): Node {
61 const controller = get_controller(renderable)
62 assert(controller._parent_node, 'the renderable has not been rendered')
63 return controller._parent_node
64}
65
66export function keyed<T extends Displayable & object>(displayable: T, key: Key): T {
67 assert(!keys.has(displayable), 'renderable already has a key')
68 keys.set(displayable, key)
69 return displayable
70}
71
72export function get_key(displayable: unknown): unknown {
73 // the cast is fine because getting any non-object will return null
74 return keys.get(displayable as object) ?? displayable
75}