@wrnrlr/prelude 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +16 -0
- package/LICENSE +1 -0
- package/deno.json +23 -0
- package/example/counter.html +24 -0
- package/example/greeting.html +25 -0
- package/example/paint.html +22 -0
- package/example/show.html +18 -0
- package/example/todo.html +70 -0
- package/index.html +230 -0
- package/package.json +12 -0
- package/presets.css +284 -0
- package/public/banner.svg +6 -0
- package/public/logo.svg +5 -0
- package/readme.md +86 -0
- package/src/canvas.js +114 -0
- package/src/components.js +20 -0
- package/src/constants.ts +515 -0
- package/src/controlflow.js +163 -0
- package/src/hyperscript.ts +237 -0
- package/src/mod.ts +45 -0
- package/src/reactive.ts +359 -0
- package/src/runtime.ts +434 -0
- package/test/hyperscript.js +102 -0
- package/test/reactive.js +98 -0
- package/test/runtime.js +7 -0
- package/typedoc.jsonc +31 -0
package/src/runtime.ts
ADDED
@@ -0,0 +1,434 @@
|
|
1
|
+
import {effect,sample,root} from './reactive.ts'
|
2
|
+
import {SVGNamespace,SVGElements,ChildProperties,getPropAlias,Properties,Aliases,DelegatedEvents} from './constants.ts'
|
3
|
+
import type {Window,Mountable,Elem,Node} from './constants.ts'
|
4
|
+
|
5
|
+
const {isArray} = Array
|
6
|
+
export const $RUNTIME = Symbol()
|
7
|
+
|
8
|
+
/**
|
9
|
+
|
10
|
+
@group Internal
|
11
|
+
*/
|
12
|
+
export type Runtime = {
|
13
|
+
// window:Window
|
14
|
+
render(code:()=>void, element:Elem, init:any): any;
|
15
|
+
insert(parent:Mountable, accessor:any, marker?:Node|null, init?:any): any;
|
16
|
+
spread(node:Elem, accessor:any, skipChildren?: boolean): void;
|
17
|
+
assign(node:Elem, props:any, skipChildren?:boolean): void;
|
18
|
+
element(name:string): any;
|
19
|
+
text(s:string): any;
|
20
|
+
isChild(a:any): boolean;
|
21
|
+
clearDelegatedEvents():void
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
|
26
|
+
@param window
|
27
|
+
@group Internal
|
28
|
+
*/
|
29
|
+
export function runtime(window:Window):Runtime {
|
30
|
+
const document = window.document,
|
31
|
+
isSVG = (e:any) => e instanceof (window.SVGElement as any),
|
32
|
+
element = (name:string) => SVGElements.has(name) ? document.createElementNS("http://www.w3.org/2000/svg",name) : document.createElement(name),
|
33
|
+
text = (s:string) => document.createTextNode(s)
|
34
|
+
|
35
|
+
function isChild(a:unknown):boolean {
|
36
|
+
return a instanceof document.Element
|
37
|
+
}
|
38
|
+
|
39
|
+
function render(code:()=>void, element:Elem, init?:any) {
|
40
|
+
if (!element) throw new Error("The `element` passed to `render(..., element)` doesn't exist.");
|
41
|
+
root(() => {
|
42
|
+
if (element === document) code()
|
43
|
+
else insert(element, code(), element.firstChild ? null : undefined, init)
|
44
|
+
})
|
45
|
+
}
|
46
|
+
|
47
|
+
function insert(parent:Mountable, accessor:any, marker?:Node|null, initial?:any) {
|
48
|
+
if (marker !== undefined && !initial) initial = []
|
49
|
+
if (!accessor.call) return insertExpression(parent, accessor, initial||[], marker)
|
50
|
+
let current = initial||[]
|
51
|
+
effect(() => {current = insertExpression(parent, accessor(), current, marker)})
|
52
|
+
}
|
53
|
+
|
54
|
+
function spread(node:Elem, props:any = {}, skipChildren:boolean) {
|
55
|
+
const prevProps:any = {}
|
56
|
+
if (!skipChildren) effect(() => (prevProps.children = insertExpression(node, props.children, prevProps.children)))
|
57
|
+
effect(() => (props.ref?.call ? sample(() => props.ref(node)) : (props.ref = node)))
|
58
|
+
effect(() => assign(node, props, true, prevProps, true))
|
59
|
+
return prevProps
|
60
|
+
}
|
61
|
+
|
62
|
+
function assign(node:Elem, props:any, skipChildren:boolean, prevProps:any = {}, skipRef:boolean = false) {
|
63
|
+
const svg = isSVG(node)
|
64
|
+
props || (props = {})
|
65
|
+
for (const prop in prevProps) {
|
66
|
+
if (!(prop in props)) {
|
67
|
+
if (prop === "children") continue;
|
68
|
+
prevProps[prop] = assignProp(node, prop, null, prevProps[prop], svg, skipRef);
|
69
|
+
}
|
70
|
+
}
|
71
|
+
for (const prop in props) {
|
72
|
+
if (prop === "children") {
|
73
|
+
if (!skipChildren) insertExpression(node, props.children);
|
74
|
+
continue;
|
75
|
+
}
|
76
|
+
const value = props[prop];
|
77
|
+
prevProps[prop] = assignProp(node, prop, value, prevProps[prop], svg, skipRef);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
function assignProp(node:Node, prop:any, value:any, prev:any, isSVG:any, skipRef:any) {
|
82
|
+
let isCE, isProp, isChildProp, propAlias, forceProp;
|
83
|
+
if (prop === "style") return style(node, value, prev);
|
84
|
+
if (prop === "classList") return classList(node, value, prev);
|
85
|
+
if (value === prev) return prev;
|
86
|
+
if (prop === "ref") {
|
87
|
+
if (!skipRef) value(node);
|
88
|
+
} else if (prop.slice(0, 3) === "on:") {
|
89
|
+
const e = prop.slice(3);
|
90
|
+
prev && node.removeEventListener(e, prev);
|
91
|
+
value && node.addEventListener(e, value);
|
92
|
+
} else if (prop.slice(0, 10) === "oncapture:") {
|
93
|
+
const e = prop.slice(10);
|
94
|
+
prev && node.removeEventListener(e, prev, true);
|
95
|
+
value && node.addEventListener(e, value, true);
|
96
|
+
} else if (prop.slice(0, 2) === "on") {
|
97
|
+
const name = prop.slice(2).toLowerCase();
|
98
|
+
const delegate = DelegatedEvents.has(name);
|
99
|
+
if (!delegate && prev) {
|
100
|
+
const h = isArray(prev) ? prev[0] : prev;
|
101
|
+
node.removeEventListener(name, h);
|
102
|
+
}
|
103
|
+
if (delegate || value) {
|
104
|
+
addEventListener(node, name, value, delegate);
|
105
|
+
delegate && delegateEvents([name],document);
|
106
|
+
}
|
107
|
+
} else if (prop.slice(0, 5) === "attr:") {
|
108
|
+
setAttribute(node, prop.slice(5), value);
|
109
|
+
} else if (
|
110
|
+
(forceProp = prop.slice(0, 5) === "prop:") ||
|
111
|
+
(isChildProp = ChildProperties.has(prop)) ||
|
112
|
+
(!isSVG && ((propAlias = getPropAlias(prop, node.tagName)) || (isProp = Properties.has(prop)))) ||
|
113
|
+
(isCE = node.nodeName.includes("-"))
|
114
|
+
) {
|
115
|
+
if (forceProp) {
|
116
|
+
prop = prop.slice(5);
|
117
|
+
isProp = true;
|
118
|
+
}
|
119
|
+
if (prop === 'class' || prop === 'className') if (value) node.className = value; else node.removeAttribute('class')
|
120
|
+
else if (isCE && !isProp && !isChildProp) node[toPropertyName(prop)] = value;
|
121
|
+
else node[propAlias || prop] = value;
|
122
|
+
} else {
|
123
|
+
const ns = isSVG && prop.indexOf(":") > -1 && SVGNamespace[prop.split(":")[0]];
|
124
|
+
if (ns) setAttributeNS(node, ns, prop, value);
|
125
|
+
else setAttribute(node, Aliases[prop] || prop, value);
|
126
|
+
}
|
127
|
+
return value;
|
128
|
+
}
|
129
|
+
|
130
|
+
function insertExpression(parent:Node, value:any, current?:any, marker?:Node, unwrapArray?:any) {
|
131
|
+
while (current?.call) current = current();
|
132
|
+
if (value === current) return current;
|
133
|
+
const t = typeof value,
|
134
|
+
multi = marker !== undefined;
|
135
|
+
parent = (multi && current[0] && current[0].parentNode) || parent;
|
136
|
+
|
137
|
+
if (t === "string" || t === "number") {
|
138
|
+
if (t === "number") {
|
139
|
+
value = value.toString();
|
140
|
+
if (value === current) return current;
|
141
|
+
}
|
142
|
+
if (multi) {
|
143
|
+
let node = current[0];
|
144
|
+
if (node && node.nodeType === 3) {
|
145
|
+
node.data !== value && (node.data = value);
|
146
|
+
} else node = document.createTextNode(value);
|
147
|
+
current = cleanChildren(parent, current, marker, node);
|
148
|
+
} else {
|
149
|
+
if (current !== "" && typeof current === "string") {
|
150
|
+
current = parent.firstChild.data = value;
|
151
|
+
} else current = parent.textContent = value;
|
152
|
+
}
|
153
|
+
} else if (value == null || t === "boolean") {
|
154
|
+
current = cleanChildren(parent, current, marker);
|
155
|
+
} else if (t === "function") {
|
156
|
+
effect(() => {
|
157
|
+
let v = value();
|
158
|
+
while (typeof v === "function") v = v();
|
159
|
+
current = insertExpression(parent, v, current, marker);
|
160
|
+
});
|
161
|
+
return () => current;
|
162
|
+
} else if (isArray(value)) {
|
163
|
+
const array:any[] = [];
|
164
|
+
const currentArray = current && isArray(current);
|
165
|
+
if (normalizeIncomingArray(array, value, current, unwrapArray)) {
|
166
|
+
effect(() => (current = insertExpression(parent, array, current, marker, true)));
|
167
|
+
return () => current;
|
168
|
+
}
|
169
|
+
if (array.length === 0) {
|
170
|
+
current = cleanChildren(parent, current, marker);
|
171
|
+
if (multi) return current;
|
172
|
+
} else if (currentArray) {
|
173
|
+
if (current.length === 0) {
|
174
|
+
appendNodes(parent, array, marker);
|
175
|
+
} else reconcileArrays(parent, current, array);
|
176
|
+
} else {
|
177
|
+
current && cleanChildren(parent);
|
178
|
+
appendNodes(parent, array);
|
179
|
+
}
|
180
|
+
current = array;
|
181
|
+
} else if (value.nodeType) {
|
182
|
+
if (isArray(current)) {
|
183
|
+
if (multi) return (current = cleanChildren(parent, current, marker, value));
|
184
|
+
cleanChildren(parent, current, null, value);
|
185
|
+
} else if (current == null || current === "" || !parent.firstChild) {
|
186
|
+
parent.appendChild(value);
|
187
|
+
} else parent.replaceChild(value, parent.firstChild);
|
188
|
+
current = value;
|
189
|
+
} else console.warn(`Unrecognized value. Skipped inserting`, value);
|
190
|
+
return current;
|
191
|
+
}
|
192
|
+
|
193
|
+
function normalizeIncomingArray(normalized:any, array:any, current:any, unwrap?:any):any {
|
194
|
+
let dynamic = false;
|
195
|
+
for (let i = 0, len = array.length; i < len; i++) {
|
196
|
+
let item = array[i]
|
197
|
+
const prev = current && current[normalized.length];
|
198
|
+
if (item == null || item === true || item === false) {
|
199
|
+
// matches null, undefined, true or false skip
|
200
|
+
} else if (typeof item === "object" && item.nodeType) {
|
201
|
+
normalized.push(item);
|
202
|
+
} else if (isArray(item)) {
|
203
|
+
dynamic = normalizeIncomingArray(normalized, item, prev) || dynamic;
|
204
|
+
} else if (item.call) {
|
205
|
+
if (unwrap) {
|
206
|
+
while (typeof item === "function") item = item();
|
207
|
+
dynamic = normalizeIncomingArray(
|
208
|
+
normalized,
|
209
|
+
isArray(item) ? item : [item],
|
210
|
+
isArray(prev) ? prev : [prev]
|
211
|
+
) || dynamic;
|
212
|
+
} else {
|
213
|
+
normalized.push(item);
|
214
|
+
dynamic = true;
|
215
|
+
}
|
216
|
+
} else {
|
217
|
+
const value = String(item);
|
218
|
+
if (prev && prev.nodeType === 3 && prev.data === value) normalized.push(prev);
|
219
|
+
else normalized.push(document.createTextNode(value));
|
220
|
+
}
|
221
|
+
}
|
222
|
+
return dynamic;
|
223
|
+
}
|
224
|
+
|
225
|
+
function cleanChildren(parent:any, current?:any, marker?:Node, replacement?:any):any {
|
226
|
+
if (marker === undefined) return (parent.textContent = "");
|
227
|
+
const node = replacement || document.createTextNode('');
|
228
|
+
if (current.length) {
|
229
|
+
let inserted = false;
|
230
|
+
for (let i = current.length - 1; i >= 0; i--) {
|
231
|
+
const el = current[i];
|
232
|
+
if (node !== el) {
|
233
|
+
const isParent = el.parentNode === parent;
|
234
|
+
if (!inserted && !i)
|
235
|
+
isParent ? parent.replaceChild(node, el) : parent.insertBefore(node, marker);
|
236
|
+
else isParent && el.remove();
|
237
|
+
} else inserted = true;
|
238
|
+
}
|
239
|
+
} else parent.insertBefore(node, marker);
|
240
|
+
return [node];
|
241
|
+
}
|
242
|
+
|
243
|
+
function clearDelegatedEvents() {
|
244
|
+
if (document[$$EVENTS]) {
|
245
|
+
for (const name of document[$$EVENTS].keys()) document.removeEventListener(name, eventHandler);
|
246
|
+
delete document[$$EVENTS];
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
return {render,insert,spread,assign,element,text,isChild,clearDelegatedEvents}
|
251
|
+
}
|
252
|
+
|
253
|
+
const $$EVENTS = "_$DX_DELEGATE"
|
254
|
+
|
255
|
+
function delegateEvents(eventNames:string[], document:any) {
|
256
|
+
const e = document[$$EVENTS] || (document[$$EVENTS] = new Set());
|
257
|
+
for (let i = 0, l = eventNames.length; i < l; i++) {
|
258
|
+
const name = eventNames[i];
|
259
|
+
if (!e.has(name)) {
|
260
|
+
e.add(name);
|
261
|
+
document.addEventListener(name, eventHandler);
|
262
|
+
}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
266
|
+
function eventHandler(e:any) {
|
267
|
+
const key = `$$${e.type}`
|
268
|
+
let node = (e.composedPath && e.composedPath()[0]) || e.target
|
269
|
+
// reverse Shadow DOM retargetting
|
270
|
+
if (e.target !== node) Object.defineProperty(e, "target", {configurable: true, value: node})
|
271
|
+
// simulate currentTarget
|
272
|
+
Object.defineProperty(e, "currentTarget", {configurable: true, get() {return node || document}})
|
273
|
+
while (node) {
|
274
|
+
const handler = node[key];
|
275
|
+
if (handler && !node.disabled) {
|
276
|
+
const data = node[`${key}Data`];
|
277
|
+
data !== undefined ? handler.call(node, data, e) : handler.call(node, e);
|
278
|
+
if (e.cancelBubble) return;
|
279
|
+
}
|
280
|
+
node = node._$host || node.parentNode || node.host;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
function setAttribute(node:Node, name:string, value?:string):any {
|
285
|
+
value ? node.setAttribute(name, value) : node.removeAttribute(name)
|
286
|
+
}
|
287
|
+
|
288
|
+
function setAttributeNS(node:Node, ns:string, name:string, value?:string):any {
|
289
|
+
value ? node.setAttributeNS(ns, name, value) : node.removeAttributeNS(ns, name)
|
290
|
+
}
|
291
|
+
|
292
|
+
function addEventListener(node:Node, name:any, handler:any, delegate:any):any {
|
293
|
+
if (delegate) {
|
294
|
+
if (isArray(handler)) {
|
295
|
+
node[`$$${name}`] = handler[0];
|
296
|
+
node[`$$${name}Data`] = handler[1];
|
297
|
+
} else node[`$$${name}`] = handler;
|
298
|
+
} else if (isArray(handler)) {
|
299
|
+
const handlerFn = handler[0];
|
300
|
+
node.addEventListener(name, (handler[0] = (e:any) => handlerFn.call(node, handler[1], e)));
|
301
|
+
} else node.addEventListener(name, handler);
|
302
|
+
}
|
303
|
+
|
304
|
+
function classList(node:Node, value:any, prev:any = {}):any {
|
305
|
+
const classKeys = Object.keys(value || {}),
|
306
|
+
prevKeys = Object.keys(prev);
|
307
|
+
let i, len;
|
308
|
+
for (i = 0, len = prevKeys.length; i < len; i++) {
|
309
|
+
const key = prevKeys[i];
|
310
|
+
if (!key || key === "undefined" || value[key]) continue;
|
311
|
+
toggleClassKey(node, key, false);
|
312
|
+
delete prev[key];
|
313
|
+
}
|
314
|
+
for (i = 0, len = classKeys.length; i < len; i++) {
|
315
|
+
const key = classKeys[i],
|
316
|
+
classValue = !!value[key];
|
317
|
+
if (!key || key === "undefined" || prev[key] === classValue || !classValue) continue;
|
318
|
+
toggleClassKey(node, key, true);
|
319
|
+
prev[key] = classValue;
|
320
|
+
}
|
321
|
+
return prev;
|
322
|
+
}
|
323
|
+
|
324
|
+
function style(node:Node, value:any, prev:any) {
|
325
|
+
if (!value) return prev ? setAttribute(node, "style") : value;
|
326
|
+
const nodeStyle = node.style;
|
327
|
+
if (typeof value === "string") return (nodeStyle.cssText = value);
|
328
|
+
if (typeof prev === "string") nodeStyle.cssText = prev = undefined
|
329
|
+
if (!prev) prev = {}
|
330
|
+
if (!value) value = {}
|
331
|
+
let v, s;
|
332
|
+
for (s in prev) {
|
333
|
+
value[s] == null && nodeStyle.removeProperty(s);
|
334
|
+
delete prev[s];
|
335
|
+
}
|
336
|
+
for (s in value) {
|
337
|
+
v = value[s];
|
338
|
+
if (v !== prev[s]) {
|
339
|
+
nodeStyle.setProperty(s, v);
|
340
|
+
prev[s] = v;
|
341
|
+
}
|
342
|
+
}
|
343
|
+
return prev;
|
344
|
+
}
|
345
|
+
|
346
|
+
function toPropertyName(name:string):string {
|
347
|
+
return name.toLowerCase().replace(/-([a-z])/g, (_:any, w:string) => w.toUpperCase());
|
348
|
+
}
|
349
|
+
|
350
|
+
function toggleClassKey(node:any, key:any, value:any) {
|
351
|
+
const classNames = key.trim().split(/\s+/)
|
352
|
+
for (let i = 0, nameLen = classNames.length; i < nameLen; i++)
|
353
|
+
node.classList.toggle(classNames[i], value)
|
354
|
+
}
|
355
|
+
|
356
|
+
function appendNodes(parent:Node, array:Node[], marker:null|Node = null) {
|
357
|
+
for (let i = 0, len = array.length; i < len; i++)
|
358
|
+
parent.insertBefore(array[i], marker)
|
359
|
+
}
|
360
|
+
|
361
|
+
// Slightly modified version of: https://github.com/WebReflection/udomdiff/blob/master/index.js
|
362
|
+
function reconcileArrays(parentNode:Node, a:Node[], b:Node[]) {
|
363
|
+
let bLength = b.length,
|
364
|
+
aEnd = a.length,
|
365
|
+
bEnd = bLength,
|
366
|
+
aStart = 0,
|
367
|
+
bStart = 0,
|
368
|
+
after = a[aEnd - 1].nextSibling,
|
369
|
+
map:Map<Node,number>|null = null;
|
370
|
+
|
371
|
+
while (aStart < aEnd || bStart < bEnd) {
|
372
|
+
// common prefix
|
373
|
+
if (a[aStart] === b[bStart]) {
|
374
|
+
aStart++;
|
375
|
+
bStart++;
|
376
|
+
continue;
|
377
|
+
}
|
378
|
+
// common suffix
|
379
|
+
while (a[aEnd - 1] === b[bEnd - 1]) {
|
380
|
+
aEnd--;
|
381
|
+
bEnd--;
|
382
|
+
}
|
383
|
+
// append
|
384
|
+
if (aEnd === aStart) {
|
385
|
+
const node =
|
386
|
+
bEnd < bLength
|
387
|
+
? bStart
|
388
|
+
? b[bStart - 1].nextSibling
|
389
|
+
: b[bEnd - bStart]
|
390
|
+
: after;
|
391
|
+
|
392
|
+
while (bStart < bEnd) parentNode.insertBefore(b[bStart++], node);
|
393
|
+
// remove
|
394
|
+
} else if (bEnd === bStart) {
|
395
|
+
while (aStart < aEnd) {
|
396
|
+
if (!map || !map.has(a[aStart])) a[aStart].remove();
|
397
|
+
aStart++;
|
398
|
+
}
|
399
|
+
// swap backward
|
400
|
+
} else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
|
401
|
+
const node = a[--aEnd].nextSibling;
|
402
|
+
parentNode.insertBefore(b[bStart++], a[aStart++].nextSibling);
|
403
|
+
parentNode.insertBefore(b[--bEnd], node);
|
404
|
+
|
405
|
+
a[aEnd] = b[bEnd];
|
406
|
+
// fallback to map
|
407
|
+
} else {
|
408
|
+
if (!map) {
|
409
|
+
map = new Map();
|
410
|
+
let i = bStart;
|
411
|
+
while (i < bEnd) map.set(b[i], i++);
|
412
|
+
}
|
413
|
+
|
414
|
+
const index = map.get(a[aStart]);
|
415
|
+
if (index != null) {
|
416
|
+
if (bStart < index && index < bEnd) {
|
417
|
+
let i = aStart,
|
418
|
+
sequence = 1,
|
419
|
+
t:number|undefined;
|
420
|
+
|
421
|
+
while (++i < aEnd && i < bEnd) {
|
422
|
+
if ((t = map.get(a[i])) == null || t !== index + sequence) break;
|
423
|
+
sequence++;
|
424
|
+
}
|
425
|
+
|
426
|
+
if (sequence > index - bStart) {
|
427
|
+
const node = a[aStart];
|
428
|
+
while (bStart < index) parentNode.insertBefore(b[bStart++], node);
|
429
|
+
} else parentNode.replaceChild(b[bStart++], a[aStart++]);
|
430
|
+
} else aStart++;
|
431
|
+
} else a[aStart++].remove();
|
432
|
+
}
|
433
|
+
}
|
434
|
+
}
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import {runtime} from '../src/runtime.ts'
|
2
|
+
import {hyperscript} from '../src/hyperscript.ts'
|
3
|
+
import {signal,root} from '../src/reactive.ts'
|
4
|
+
import {JSDOM} from 'npm:jsdom'
|
5
|
+
import {assertEquals} from '@std/assert'
|
6
|
+
|
7
|
+
const {window} = new JSDOM('<!DOCTYPE html>', {runScripts:'dangerously'})
|
8
|
+
const {document,MouseEvent} = window
|
9
|
+
const r = runtime(window), h = hyperscript(r)
|
10
|
+
|
11
|
+
function testing(name, props, f=props) {
|
12
|
+
const htest = t => (name, f) => {
|
13
|
+
let disposer
|
14
|
+
return root(dispose => {
|
15
|
+
disposer = () => {
|
16
|
+
document.body.textContent = ''
|
17
|
+
r.clearDelegatedEvents()
|
18
|
+
dispose()
|
19
|
+
}
|
20
|
+
return t.step(name, f).then(disposer)
|
21
|
+
})
|
22
|
+
}
|
23
|
+
Deno.test(name, async t => await f(htest(t)))
|
24
|
+
}
|
25
|
+
|
26
|
+
function assertHTML(t, e, msg) { assertEquals(t().outerHTML, e, msg) }
|
27
|
+
|
28
|
+
testing('h with basic element', {skip:true}, async test => {
|
29
|
+
await test('empty tag', () => assertHTML(h(''), '<div></div>'))
|
30
|
+
await test('tag with id', () => assertHTML(h('#a'), '<div id="a"></div>'))
|
31
|
+
await test('tag with class', () => assertHTML(h('.a'), '<div class="a"></div>'))
|
32
|
+
await test('tag with id & classes', () => assertHTML(h('i#a.b.c'), '<i id="a" class="b c"></i>'))
|
33
|
+
await test('no props or children', () => assertHTML(h('hr'), '<hr>'))
|
34
|
+
await test('boolean content', () => assertHTML(h('b',true), '<b>true</b>'))
|
35
|
+
await test('string content', () => assertHTML(h('b','A'), '<b>A</b>'))
|
36
|
+
await test("number content", () => assertHTML(h('i',1), '<i>1</i>'))
|
37
|
+
await test("bigint content", () => assertHTML(h('i',2n), '<i>2</i>'))
|
38
|
+
await test("symbol content", () => assertHTML(h('i',Symbol('A')), '<i>Symbol(A)</i>'))
|
39
|
+
await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
|
40
|
+
await test("signal content", () => assertHTML(h('i',()=>1), '<i>1</i>'))
|
41
|
+
await test('array content', () => assertHTML(h('i',['A',1,2n]), '<i>A12</i>'))
|
42
|
+
await test('style attribute', () => assertHTML(h('hr',{style:'color:red'}), '<hr style="color: red;">'))
|
43
|
+
await test('htmlFor attribute', () => assertHTML(h('label',{htmlFor:'a'}), '<label for="a"></label>'))
|
44
|
+
await test('ref attribute', () => assertHTML(h('hr',{ref:el=>el.setAttribute('a','1')}), '<hr a="1">'))
|
45
|
+
await test('classList attribute', () => assertHTML(h('hr', {classList:()=>({a:true})}), '<hr class="a">'))
|
46
|
+
await test('class from tag & attribute', () => assertHTML(h('hr.a',{class:'b'}), '<hr class="a b">'))
|
47
|
+
})
|
48
|
+
|
49
|
+
function assertText(t, e, msg) { assertEquals(t(), e, msg) }
|
50
|
+
|
51
|
+
// testing('h with fragment', { skip: true }, async test => {
|
52
|
+
// await test('boolean fragment', () => assertText(h([true]), 'true'))
|
53
|
+
// await test('string fragment', () => assertText(h(['A']), 'A'))
|
54
|
+
// await test('number fragment', () => assertText(h([1]), '1'))
|
55
|
+
// await test('bigint fragment', () => assertText(h([2n]), '2'))
|
56
|
+
// await test('array fragment', () => assertText(h(p=>p.children,['A',1,2n]), 'A12'))
|
57
|
+
// await test('array fragment', () => assertText(h(p=>p.children,['A',1,2n,()=>true]), 'A12'))
|
58
|
+
// await test('signal fragment', () => assertText(h([()=>1]), '1'))
|
59
|
+
// })
|
60
|
+
|
61
|
+
testing('h with reactive content', {skip:true}, async test => {
|
62
|
+
await test('higher-order component', () => {
|
63
|
+
const Hi = p => h('b',['Hi ',p.name]),
|
64
|
+
name = signal('An'),
|
65
|
+
t = h(Hi, {name:()=>name})()
|
66
|
+
assertHTML(t,`<b>Hi An</b>`)
|
67
|
+
name('Yi')
|
68
|
+
assertHTML(t,`<b>Hi Yi</b>`)
|
69
|
+
})
|
70
|
+
})
|
71
|
+
|
72
|
+
// testing('h with event handler', {skip:true}, async test => {
|
73
|
+
// const i = 0
|
74
|
+
// const t = h('button', {onclick:()=>i+=1})
|
75
|
+
// console.log(t())//.firstChild.dispatchEvent(new MouseEvent('click',{bubbles:true}))
|
76
|
+
// t().firstChild.dispatchEvent(new MouseEvent('click',{bubbles:true}))
|
77
|
+
// assertEquals(i, 1)
|
78
|
+
// })
|
79
|
+
|
80
|
+
// describe("Components", () => {
|
81
|
+
// const Comp = props => h("div", () => props.name + " " + props.middle, props.children);
|
82
|
+
// S.root(() => {
|
83
|
+
// const template = h("#main", [
|
84
|
+
// h(Comp, { name: () => "John", middle: "R." }, () => h("span", "Smith"))
|
85
|
+
// ])();
|
86
|
+
// const div = document.createElement("div");
|
87
|
+
// div.appendChild(template);
|
88
|
+
// assertEquals(div.innerHTML,FIXTURES[4])
|
89
|
+
// });
|
90
|
+
// });
|
91
|
+
|
92
|
+
// describe("Component Spread", () => {
|
93
|
+
// const Comp = props => h("div", props);
|
94
|
+
// S.root(() => {
|
95
|
+
// const template = h("#main", [
|
96
|
+
// h(Comp, { name: () => "John" }, () => h("span", "Smith"))
|
97
|
+
// ])();
|
98
|
+
// const div = document.createElement("div");
|
99
|
+
// div.appendChild(template);
|
100
|
+
// assertEquals(div.innerHTML,FIXTURES[5])
|
101
|
+
// });
|
102
|
+
// });
|
package/test/reactive.js
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
import {assertEquals,assert} from '@std/assert'
|
2
|
+
import {describe,it} from '@std/testing/bdd'
|
3
|
+
|
4
|
+
import {signal,effect,sample,batch,memo,context} from '../src/reactive.ts'
|
5
|
+
import {wrap} from '../src/controlflow.js'
|
6
|
+
|
7
|
+
describe('signal', () => {
|
8
|
+
const a = signal(1)
|
9
|
+
assertEquals(a(),1)
|
10
|
+
assertEquals(a(a()+1),2)
|
11
|
+
assertEquals(a(i=>i+1),3)
|
12
|
+
assertEquals(a('Hi'),'Hi')
|
13
|
+
assertEquals(a(NaN),NaN)
|
14
|
+
assertEquals(a(null),null)
|
15
|
+
})
|
16
|
+
|
17
|
+
describe('effect', () => {
|
18
|
+
const n = signal(1)
|
19
|
+
let m = 0
|
20
|
+
// effect(initial => m = n() + initial,1)
|
21
|
+
effect(() => m = n() + 1)
|
22
|
+
assertEquals(m,2)
|
23
|
+
n(2)
|
24
|
+
assertEquals(m,3)
|
25
|
+
})
|
26
|
+
|
27
|
+
describe('sample',()=>{
|
28
|
+
const n = signal(1)
|
29
|
+
let m = 0
|
30
|
+
effect(()=>m = sample(n))
|
31
|
+
assertEquals(m,1)
|
32
|
+
n(2)
|
33
|
+
assertEquals(m,1)
|
34
|
+
})
|
35
|
+
|
36
|
+
describe('computed',()=>{
|
37
|
+
const a = signal(1),
|
38
|
+
b = signal(10),
|
39
|
+
c = ()=>a()+b()
|
40
|
+
assertEquals(c(),11)
|
41
|
+
a(2)
|
42
|
+
assertEquals(c(),12)
|
43
|
+
b(20)
|
44
|
+
assertEquals(c(),22)
|
45
|
+
let i = 0
|
46
|
+
effect(()=>{c(); i++})
|
47
|
+
batch(()=>{a(v => v++); b(v=>v++)})
|
48
|
+
})
|
49
|
+
|
50
|
+
describe('batch', () => {
|
51
|
+
const a = signal(1), b = signal(1)
|
52
|
+
let i = 0
|
53
|
+
effect(()=>{a(); b(); i++})
|
54
|
+
assertEquals(i,1)
|
55
|
+
a(2)
|
56
|
+
b(2)
|
57
|
+
assertEquals(i,3)
|
58
|
+
batch(()=>{a(1); b(1)})
|
59
|
+
it('batches changes', () => assertEquals(i,4))
|
60
|
+
})
|
61
|
+
|
62
|
+
describe('memo',{skip:true},() => {
|
63
|
+
describe('memo with initial value',() => {})
|
64
|
+
})
|
65
|
+
|
66
|
+
describe('wrap',()=>{
|
67
|
+
describe('wrap singal of array', () => {
|
68
|
+
const all = signal(['a','b']), first = wrap(all,0)
|
69
|
+
assertEquals(first(),'a')
|
70
|
+
assertEquals(first('A'),'A')
|
71
|
+
assertEquals(first(),'A')
|
72
|
+
})
|
73
|
+
|
74
|
+
describe('wrap singal of object', () => {
|
75
|
+
const all = signal({name:'a'}), name = wrap(all,'name')
|
76
|
+
assertEquals(name(),'a')
|
77
|
+
assertEquals(name('A'),'A')
|
78
|
+
assertEquals(name(),'A')
|
79
|
+
})
|
80
|
+
|
81
|
+
describe('wrap singal of array of objects', () => {
|
82
|
+
const all = signal([{name:'a'}]), first = wrap(all,0), name = wrap(first,'name')
|
83
|
+
assertEquals(first({name:'b'}),{name:'b'})
|
84
|
+
assertEquals(first(),{name:'b'})
|
85
|
+
assertEquals(name(),'b')
|
86
|
+
assertEquals(name('A'),'A')
|
87
|
+
assertEquals(name(),'A')
|
88
|
+
})
|
89
|
+
|
90
|
+
describe('wrap singal of object of arrays', () => {
|
91
|
+
const all = signal({ids:[0,1,2]}), ids = wrap(all,'id'), last = wrap(ids,-1)
|
92
|
+
assertEquals(ids([1,2,3]),[1,2,3])
|
93
|
+
assertEquals(ids(),[1,2,3])
|
94
|
+
assertEquals(last(),3)
|
95
|
+
assertEquals(last(4),4)
|
96
|
+
assertEquals(last(),4)
|
97
|
+
})
|
98
|
+
})
|
package/test/runtime.js
ADDED
package/typedoc.jsonc
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
{
|
2
|
+
"name": "prelude",
|
3
|
+
"hostedBaseUrl": "https://wrnrlr.github.io/prelude",
|
4
|
+
"useHostedBaseUrlForAbsoluteLinks": false,
|
5
|
+
"out":"./public/docs/",
|
6
|
+
"compilerOptions": {
|
7
|
+
"strict": true,
|
8
|
+
"allowJs": true,
|
9
|
+
"checkJs": false,
|
10
|
+
"noImplicitThis": false,
|
11
|
+
"allowImportingTsExtensions": true,
|
12
|
+
"lib": [],
|
13
|
+
"target": ["esnext"],
|
14
|
+
"downlevelIteration": true
|
15
|
+
},
|
16
|
+
"entryPoints": ["./src/mod.ts"],
|
17
|
+
"navigation": {
|
18
|
+
"includeCategories": true,
|
19
|
+
"includeGroups": false,
|
20
|
+
"includeFolders": true
|
21
|
+
},
|
22
|
+
"visibilityFilters": {
|
23
|
+
"protected": false,
|
24
|
+
"private": false,
|
25
|
+
"inherited": true,
|
26
|
+
"external": false,
|
27
|
+
"@alpha": false,
|
28
|
+
"@beta": false
|
29
|
+
},
|
30
|
+
"categorizeByGroup": true
|
31
|
+
}
|