-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
import { effect, Signal } from "./signal.js";
export type ElementFn<T extends HTMLElement | SVGElement> = (el: T) => void;
// TypeScript somehow rejects Signal<HTMLElement | SVGELement> (maybe due to their web typings?)
// A | B | C -> Signal<A> | Signal<B> | Signal<C>
type ToSignal<T> = T extends any ? Signal<T> : never;
type AttrValue = string | boolean;
/**
* Set or remove an attribute.
*
* @param name - An attribute name.
* @param value - `string` is set as-is. `boolean` follows HTML's boolean attribute semantics:
* `true` sets an empty string and `false` removes the attribute itself.
*/
export function attr<T extends HTMLElement | SVGElement>(
name: string,
value: AttrValue | ToSignal<AttrValue> | Signal<AttrValue>,
): ElementFn<T> {
return (el) => {
if (value instanceof Signal) {
effect(() => {
const v = value.get();
if (typeof v === "string") {
el.setAttribute(name, v);
} else if (v === true) {
el.setAttribute(name, "");
} else {
el.removeAttribute(name);
}
});
} else if (typeof value === "string") {
el.setAttribute(name, value);
} else if (value === true) {
el.setAttribute(name, "");
}
};
}
/**
* Assign a value to the property.
*/
export function prop<T extends HTMLElement | SVGElement, K extends keyof T>(
key: K,
value: T[K] | Signal<T[K]>,
): ElementFn<T> {
return (el) => {
if (value instanceof Signal) {
effect(() => {
el[key] = value.get();
});
} else {
el[key] = value;
}
};
}
/**
* Invoke the given callback after `requestAnimationFrame`.
*
* Provided as an escape-hatch for DOM quirks.
*
* @example
* el("select", [
* raf(compute(() => (el) => {
* el.value = value.get();
* }))
* ])
*/
export function raf<T extends HTMLElement | SVGElement>(
f: ((el: T) => void) | Signal<(el: T) => void>,
): ElementFn<T> {
return (el) => {
requestAnimationFrame(() => {
if (f instanceof Signal) {
effect(() => {
f.get()(el);
});
} else {
f(el);
}
});
};
}
/**
* Set element's inline style.
*
* This is not same as `HTMLElement.style.foo`: under the hood, `CSSStyleDeclaration.setProperty` is used.
* Hence, property name must be hyphen-cased.
* Property value can be one of `string`, `null`, or `undefined`.
*
* - `string` ... Sets the value to the property.
* - `null` ... Removes the property from stylesheet.
* - `undefined` ... Does nothing.
*
* When used with Signal, use of `undefined` would lead to confusing behavor.
*
* ```ts
* const border = signal<string | undefined>("1px solid #000");
* style({ border });
* border.set(undefined)
* ```
*
* In the above code, setting `undefined` does nothing: the actual border property's value
* is still `1px solid #000`. In order to avoid these kind of surprising situation, use of
* `string` is always recommended.
*
* ```ts
* const border = signal("1px solid #000");
* style({ border });
* border.set("none")
* ```
*/
export function style<T extends HTMLElement | SVGElement>(
style: Record<
string,
string | null | undefined | Signal<string | null | undefined>
>,
): ElementFn<T> {
return (el) => {
for (const key in style) {
const value = style[key];
if (typeof value === "string") {
el.style.setProperty(key, value);
} else if (value instanceof Signal) {
effect(() => {
const v = value.get();
if (typeof v === "string") {
el.style.setProperty(key, v);
} else if (v === null) {
el.style.removeProperty(key);
}
});
} else if (value === null) {
el.style.removeProperty(key);
}
}
};
}
/**
* Sets a class or a list of classes.
*
* This function does not accept Signal.
* Use `data-*` attribute or property for dynamic values.
*/
export function className<T extends HTMLElement | SVGElement>(
...value: readonly string[]
): ElementFn<T> {
return (el) => {
el.classList.add(...value);
};
}
/**
* Attach an event listener.
*/
export function on<T extends HTMLElement, E extends keyof HTMLElementEventMap>(
eventName: E,
callback: (event: HTMLElementEventMap[E]) => void,
options?: AddEventListenerOptions,
): ElementFn<HTMLElement>;
export function on<T extends SVGElement, E extends keyof SVGElementEventMap>(
eventName: E,
callback: (event: SVGElementEventMap[E]) => void,
options?: AddEventListenerOptions,
): ElementFn<SVGElement>;
export function on<
T extends HTMLElement | SVGElement,
E extends keyof HTMLElementEventMap | keyof SVGElementEventMap,
>(
eventName: E,
callback: (event: (HTMLElementEventMap & SVGElementEventMap)[E]) => void,
options?: AddEventListenerOptions,
): ElementFn<T> {
return (el) => {
// @ts-expect-error: This is a limit coming from TS being dirty hack illusion.
el.addEventListener(eventName, callback, options);
};
}
type ElementChild = HTMLElement | SVGElement | string | null | undefined;
function appendChild(parent: Element, child: ElementChild): void {
if (child === null || typeof child === "undefined") {
return;
}
if (typeof child === "string") {
parent.appendChild(document.createTextNode(child));
} else {
parent.appendChild(child);
}
}
// `el` is parameterized because a function to create an `Element` depends on Element types. (sub-types?)
function provision<T extends HTMLElement | SVGElement>(
el: T,
attrs: readonly ElementFn<T>[],
children: readonly (
| ElementChild
| ToSignal<ElementChild>
| Signal<ElementChild>
)[],
): T {
for (const attr of attrs) {
attr(el);
}
for (const child of children) {
if (child instanceof Signal) {
const start = document.createTextNode("");
const end = document.createTextNode("");
el.appendChild(start);
el.appendChild(end);
effect(() => {
const childNode = child.get();
const prevNode =
!start.nextSibling || start.nextSibling === end
? null
: start.nextSibling;
if (childNode === null || typeof childNode === "undefined") {
if (prevNode) {
prevNode.remove();
}
return;
}
const node =
typeof childNode === "string"
? document.createTextNode(childNode)
: childNode;
if (prevNode) {
prevNode.replaceWith(node);
} else {
el.insertBefore(node, end);
}
});
} else {
appendChild(el, child);
}
}
return el;
}
/**
* Create a HTML element.
*/
export function el<TagName extends keyof HTMLElementTagNameMap>(
tagName: TagName,
attrs: readonly ElementFn<HTMLElementTagNameMap[TagName]>[] = [],
children: readonly (
| ElementChild
| ToSignal<ElementChild>
| Signal<ElementChild>
)[] = [],
): HTMLElementTagNameMap[TagName] {
return provision(document.createElement(tagName), attrs, children);
}
/**
* Create a SVG element.
*
* You don't need to set `xmlns` attribute for elements created by this function.
*/
export function svg<TagName extends keyof SVGElementTagNameMap>(
tagName: TagName,
attrs: readonly ElementFn<SVGElementTagNameMap[TagName]>[] = [],
children: readonly (
| ElementChild
| ToSignal<ElementChild>
| Signal<ElementChild>
)[] = [],
): SVGElementTagNameMap[TagName] {
return provision(
document.createElementNS("http://www.w3.org/2000/svg", tagName),
attrs,
children,
);
}