@potch/munifw 1.0.0
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/README.md +170 -0
- package/build.sh +70 -0
- package/dist/munifw.min.js +1 -0
- package/dist/munifw.min.js.gz +0 -0
- package/dist/ssr.min.js +1 -0
- package/dist/ssr.min.js.gz +0 -0
- package/package.json +19 -0
- package/sizes.md +6 -0
- package/src/munifw.js +205 -0
- package/src/ssr.js +99 -0
- package/test/munifw.test.js +292 -0
- package/test/setup.js +13 -0
- package/test/ssr.test.js +64 -0
- package/vitest.config.js +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# munifw
|
|
2
|
+
|
|
3
|
+
A signal-driven framework for small applications
|
|
4
|
+
|
|
5
|
+
[current file sizes](./sizes.md)
|
|
6
|
+
|
|
7
|
+
# API
|
|
8
|
+
|
|
9
|
+
## `event(watchers?) => [emitEvent, onEvent]`
|
|
10
|
+
|
|
11
|
+
Create an event bus. The bus consists of two methods, an `emit` and an `on` method that notify and register a set of `watchers`. Calls to `on` return a `teardown` function used to remove the listener.
|
|
12
|
+
|
|
13
|
+
### Arguments
|
|
14
|
+
|
|
15
|
+
- `watchers` - optional `Set` containing `callback` functions that will be called by `emitEvent`
|
|
16
|
+
|
|
17
|
+
### Returns
|
|
18
|
+
|
|
19
|
+
`Array` containing two methods:
|
|
20
|
+
|
|
21
|
+
- `emitEvent(data?)` - calls all `callback` functions with the provided optional `data`.
|
|
22
|
+
- `onEvent(callback) => teardown` - adds `callback` to the `watchers` set. Returns a `teardown` function to remove `callback` from `watchers` set
|
|
23
|
+
|
|
24
|
+
### Example
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
function clock() {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const [onUpdate, emitUpdate] = event();
|
|
30
|
+
setInterval(() => {
|
|
31
|
+
emitUpdate(start - Date.now());
|
|
32
|
+
}, 1000);
|
|
33
|
+
return emitUpdate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const onClock = clock();
|
|
37
|
+
const stopClockLogs = onClock((time) => {
|
|
38
|
+
console.log(`time elapsed: ${time}ms`);
|
|
39
|
+
if (time > 30 * 1000) {
|
|
40
|
+
stopClockLogs();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## `signal(initialValue, equality?) => <signal>`
|
|
46
|
+
|
|
47
|
+
Reactive state primitive built on top of `event`. Setting `.value` updates all `computed` values and runs any `effects` that depend on `<signal>.value`
|
|
48
|
+
|
|
49
|
+
### Arguments
|
|
50
|
+
|
|
51
|
+
- `initialValue` - initial value of the signal's `value` property.
|
|
52
|
+
- `equality: (a, b) => boolean` - optional function used to compare subsequent values of `<signal>`.value. Signals only update if `equality` returns false (indicating a change). Default equality check is `===`.
|
|
53
|
+
|
|
54
|
+
### Returns
|
|
55
|
+
|
|
56
|
+
A `<signal>` interface with the following properties:
|
|
57
|
+
|
|
58
|
+
- `.watch(fn) => teardown` - add `fn` as a watcher to be notified when the state of the signal changes. Returns a `teardown` function that removes the watcher when called.
|
|
59
|
+
- `.value`- state of the signal. backed by a pair of getters and setters such that setting `.value` triggers an update and accessing `.value` inside an `effect` automatically registers a watcher.
|
|
60
|
+
- `.touch()` manually trigger all watchers
|
|
61
|
+
- `.peek()` returns current `.value` without registering a watcher within `computed` or `effect`
|
|
62
|
+
|
|
63
|
+
### Example
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const s = signal(1);
|
|
67
|
+
|
|
68
|
+
s.watch(() => console.log("value is", s.value));
|
|
69
|
+
|
|
70
|
+
s.value = 1; // watcher will not run, value is the same
|
|
71
|
+
s.value = 2; // watcher function will log
|
|
72
|
+
|
|
73
|
+
const person = signal({ name: "bob", age: 29 }, (a, b) => a.name === b.name);
|
|
74
|
+
|
|
75
|
+
person.watch(() => console.log("new person!", person.value.name));
|
|
76
|
+
|
|
77
|
+
person.value = { name: "bob", age: 30 }; // no log
|
|
78
|
+
person.value = { name: "alice", age: 30 }; // log new person!
|
|
79
|
+
|
|
80
|
+
person.value.name = "pat"; // no log bc value wasn't changed
|
|
81
|
+
person.touch(); // triggers log
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## `effect(fn, ...explicitDependencies) => teardown`
|
|
85
|
+
|
|
86
|
+
A reactive effect that runs whenever any of its dependencies change.
|
|
87
|
+
|
|
88
|
+
### Arguments
|
|
89
|
+
|
|
90
|
+
- `fn` - the body of the effect, using any number of `signal` or `computed` values to perform other work. The callback is called once immediately on the creation of the effect and any time any depnendencies (signal or computed) are changed. References to `<signal>.value` or `<computed>.value` encountered during this initial run of the callback will automatically be detected as dependencies of the effect and will re-run the effect whenever they change. `fn` is called with a special `innerEffect` argument that, if used instead of global `effect`, will ensure that effects created inside of `fn` will be destroyed when the outer effect `teardown` is called.
|
|
91
|
+
- `explicitDependencies` - Dependencies that are not encountered on the initial callback run can be passed positionally as `explicitDependencies` to ensure they are registered.
|
|
92
|
+
|
|
93
|
+
### Returns
|
|
94
|
+
|
|
95
|
+
A `teardown` function that can be called to "destroy" the effect and stop watching all its dependencies.
|
|
96
|
+
|
|
97
|
+
## `computed(fn, ...explicitDependencies) => <computed>`
|
|
98
|
+
|
|
99
|
+
A derived reactive value, internally the composition of a `signal` and an `effect`. `.value` updates when any signal/computed referenced in `fn` by `.value` changes. Takes the same arguments as `effect`.
|
|
100
|
+
|
|
101
|
+
### Returns
|
|
102
|
+
|
|
103
|
+
A `<computed>` interface, which is the same as `<signal>` but with an additional `.teardown()` method which is a reference to the teardown provided by the internal `effect`.
|
|
104
|
+
|
|
105
|
+
### Example
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const a = signal(1);
|
|
109
|
+
const b = signal(2);
|
|
110
|
+
|
|
111
|
+
const c = computed(() => a.value + b.value);
|
|
112
|
+
|
|
113
|
+
console.log(c.value); // 3
|
|
114
|
+
|
|
115
|
+
a.value = 3;
|
|
116
|
+
|
|
117
|
+
console.log(c.value); // 5
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## `dom(tag, props?, ...children) => HTMLElement`
|
|
121
|
+
|
|
122
|
+
Create HTML elements, hyperscript compatible. Effects created during prop setting are captured and reported to `onEvent`.
|
|
123
|
+
|
|
124
|
+
### Arguments
|
|
125
|
+
|
|
126
|
+
- `tag` - can be a string tag name or a function that returns an HTMLElement
|
|
127
|
+
- `props` - optional object of properties/attributes. Attached to the element using `assign` and `setProp`. If `props` is not an object it's treated as the first chid.
|
|
128
|
+
- `children` - positional arguments, appended to created element.
|
|
129
|
+
|
|
130
|
+
### Returns
|
|
131
|
+
|
|
132
|
+
Created `HTMLElement`.
|
|
133
|
+
|
|
134
|
+
## `on(el, type, handler) => teardown`
|
|
135
|
+
|
|
136
|
+
Create DOM event listeners using `addEventListener`, returns a teardown that calls `removeEventListener`
|
|
137
|
+
|
|
138
|
+
## `mount(fn, ...explicitDependencies) => HTMLElement?`
|
|
139
|
+
|
|
140
|
+
## `assign(a, b) => a`
|
|
141
|
+
|
|
142
|
+
Deep-recursing version of `Object.assign`. If `a` is an `HTMLElement`, uses `setProp`.
|
|
143
|
+
|
|
144
|
+
## `setProp(el, key, value) => void`
|
|
145
|
+
|
|
146
|
+
Set a single prop on an element. Props values can be signals/computeds, which will be bound to the element and automatically updated. If the special prop `ref` is a signal, that signal's value will be set to the element. Functions, objects, and existing props of the element are set directly on the element. Other values are treated as attributes and set using `setAttribute` or `removeAttribute`
|
|
147
|
+
|
|
148
|
+
## `onEffect(fn) => teardown`
|
|
149
|
+
|
|
150
|
+
A global event bus that is called whenever any framework method creates an effect. Listeners are called with the teardown associated with that effect. Used for ensuring internal effects can be tracked and cleaned up.
|
|
151
|
+
|
|
152
|
+
## `using(cb, fn)`
|
|
153
|
+
|
|
154
|
+
Runs `fn` and automatically calls `cb` after `fn` returns.
|
|
155
|
+
|
|
156
|
+
## `collect(onEvent, fn, collection?)`
|
|
157
|
+
|
|
158
|
+
Uses the provided event bus and `using` to "collect" any arguments passed to events emitted when `fn` is run.
|
|
159
|
+
|
|
160
|
+
### Example
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const [emit, on] = event();
|
|
164
|
+
const teardowns = collect(on, () => {
|
|
165
|
+
for (let i = 1; i <= 4; i++) {
|
|
166
|
+
emit(i);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
console.log(teardowns); // [1, 2, 3, 4]
|
|
170
|
+
```
|
package/build.sh
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
|
|
3
|
+
OUTPUT="./dist"
|
|
4
|
+
COLSIZE=7
|
|
5
|
+
TABLETEMP=$(mktemp)
|
|
6
|
+
TERMTEMP=$(mktemp)
|
|
7
|
+
|
|
8
|
+
function len() {
|
|
9
|
+
# strip escape chars
|
|
10
|
+
TEXT_ESC=$(echo $1 | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g')
|
|
11
|
+
echo $TEXT_ESC | wc -c | grep -Eo "\d+"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pad() {
|
|
15
|
+
TEXT=$1
|
|
16
|
+
LEN=$2
|
|
17
|
+
RALIGN=$3
|
|
18
|
+
TEXT_LEN=$(len $TEXT)
|
|
19
|
+
REMAIN=$(printf ' %.0s' {0..$(($LEN - $TEXT_LEN))})
|
|
20
|
+
if [[ -n $RALIGN ]] then;
|
|
21
|
+
echo "$REMAIN$TEXT"
|
|
22
|
+
else
|
|
23
|
+
echo "$TEXT$REMAIN"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function max() {
|
|
28
|
+
echo $(( $1 > $2 ? $1 : $2))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function leftright() {
|
|
32
|
+
TEXT_LEN=$(len "$2")
|
|
33
|
+
echo "$(pad $1 $(($COLUMNS - $TEXT_LEN)))$2"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sizeof() {
|
|
37
|
+
echo "$(cat $1 | wc -c | grep -Eo "\d+")"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function build() {
|
|
41
|
+
MINFILE=`basename $1 | sed 's/\(.*\.\)js/\1min.js/'`
|
|
42
|
+
cat $1 | terser -c "booleans_as_integers=true,passes=2" \
|
|
43
|
+
-m "reserved=['_','$','\$$','on','dom']" \
|
|
44
|
+
--module \
|
|
45
|
+
--ecma 2020 > "$OUTPUT/$MINFILE"
|
|
46
|
+
cat "$OUTPUT/$MINFILE" | gzip > "$OUTPUT/$MINFILE.gz"
|
|
47
|
+
MINSIZE=$(sizeof $OUTPUT/$MINFILE)
|
|
48
|
+
ZIPSIZE=$(sizeof $OUTPUT/$MINFILE.gz)
|
|
49
|
+
leftright "\e[37;1m$1\e[0m" "$(pad $(sizeof $1) $COLSIZE 1)$(pad "\e[33m$MINSIZE\e[0m" $COLSIZE 1)$(pad "\e[33m$ZIPSIZE\e[0m" $COLSIZE 1)" >> "$TERMTEMP"
|
|
50
|
+
echo "$(pad $1 20) | $(pad $(sizeof $1) 9 1) | $(pad $MINSIZE 9 1) | $(pad $ZIPSIZE 6 1)" >> "$TABLETEMP"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
echo "\nbuilding...\n"
|
|
54
|
+
|
|
55
|
+
leftright "file" "$(pad "orig" $COLSIZE 1)$(pad "min" $COLSIZE 1)$(pad "gz" $COLSIZE 1)"
|
|
56
|
+
echo $(printf '-%.0s' {0..$(($COLUMNS - 1))})
|
|
57
|
+
for file in $(ls src/*.js)
|
|
58
|
+
do
|
|
59
|
+
build "$file" &
|
|
60
|
+
done
|
|
61
|
+
wait
|
|
62
|
+
cat "$TERMTEMP" | sort
|
|
63
|
+
|
|
64
|
+
echo "# file sizes\n" > sizes.md
|
|
65
|
+
echo "$(pad "file" 20) | $(pad "original" 9 1) | $(pad "minified" 9 1) | $(pad "gzip" 6 1)" >> sizes.md
|
|
66
|
+
echo "$(pad ":---" 20) | $(pad "---:" 9 1) | $(pad "---:" 9 1) | $(pad "---:" 6 1) " >> sizes.md
|
|
67
|
+
cat "$TABLETEMP" | sort >> sizes.md
|
|
68
|
+
|
|
69
|
+
echo "\ndone!"
|
|
70
|
+
exit 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t="value",e=t=>"object"==typeof t,n=(t,e)=>[...t].map(e);export const using=(t,e)=>(e(),t());export const collect=(t,e,n=[])=>(using(t(t=>n.push(t)),e),n);export const event=(t=new Set)=>[(...e)=>n(t,t=>t(...e)),e=>(t.add(e),()=>t.delete(e))];const o=[];export const signal=(e,n=(t,e)=>t===e)=>{const[c,s]=event();return{set[t](t){n(e,t)||(e=t,c())},touch(){c()},get[t](){return o[0]&&o[0].add(this),e},peek:()=>e,watch:s}};export const[emitEffect,onEffect]=event();export const effect=(t,...e)=>{let c=0;o.push(new Set(e));const s=()=>{c||(c=1,t((...t)=>emitEffect(effect(...t))),c=0)};s();const r=n(o.at(-1),t=>t.watch(s));return o.pop(),()=>n(r,t=>t())};export const computed=(e,...n)=>{const o=signal();return o.teardown=effect(()=>o[t]=e(o[t]),...n),o};export const setProp=(n,o,c)=>{"ref"==o&&t in c?c[t]=n:e(c)&&t in c?emitEffect(effect(()=>setProp(n,o,c[t]))):"function"==typeof c||e(c)||o in n?n[o]&&e(n[o])?assign(n[o],c):n[o]=null===c?"":c:null!==c&&(0!=c||o.startsWith("data-")||o.startsWith("aria-"))?n.setAttribute(o,c):n.removeAttribute(o)};export const assign=(t,n)=>(n&&Object.entries(n).forEach(([n,o])=>{t.nodeType?setProp(t,n,o):e(t[n])&&e(o)?assign(t[n],o):t[n]=o}),t);export const dom=(t,n,...o)=>{let c;return"function"==typeof t?c=mount(()=>t(n,o)):(c=document.createElement(t),n&&e(n)&&!n.nodeType?assign(c,n):n&&o.unshift(n),c.append(...o.flat(1))),c};export const mount=(t,...e)=>{const n=[];let o;return emitEffect(effect(()=>{for(;n.length;)try{n.pop()()}catch(t){}collect(onEffect,()=>{const e=t();o&&e&&o.replaceWith(e),o=e},n)},...e)),o};export const on=(t,...e)=>(t.addEventListener(...e),()=>t.removeEventListener(...e));
|
|
Binary file
|
package/dist/ssr.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=/^(area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/,t="attributes",r="parentNode",i="childNodes",n=Object,s=(e,t)=>e[i].map(t),o={append(...e){e.map(e=>{e=e?.nodeType?e:a(3,{textContent:""+e}),this[i].push(e),e[r]=this})},replaceWith(e){const t=this[r];t&&(t[i]=s(t,t=>t==this?e:t),e[r]=t)},replaceChildren(...e){s(this,e=>e.remove()),this.append(...e)},setAttribute(e,r){this[t][e]=r},getAttribute(e){return this[t][e]},removeAttribute(e){delete this[t][e]},remove(){const e=this,t=e[r];t&&(t[i]=t[i].filter(t=>t!=e),e[r]=null)},get outerHTML(){const r=this,{tagName:i,nodeType:s}=r;return 1==s?"<"+i+n.entries(r[t]).map(e=>` ${e[0]}="${e[1]}"`).join("")+">"+r.innerHTML+(e.test(i)?"":`</${i}>`):3==s?r.textContent:""},get innerHTML(){return s(this,e=>e.outerHTML).join("")}},a=(e,t)=>n.assign(n.create(o),{nodeType:e},t);export default{createElement:e=>a(1,{tagName:e.toLowerCase(),[t]:{},[i]:[]}),find:(e,r)=>{let n,s=[e];do{if(n=s.pop(),n?.[t]?.id==r)return n;n&&s.push(...n[i])}while(s.length)}};
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@potch/munifw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A signal-driven framework for small applications",
|
|
5
|
+
"main": "src/munifw.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"start": "npx onchange -ik 'src/*.js' 'test/*.test.js' build.sh -- npm run dev",
|
|
9
|
+
"build": "./build.sh",
|
|
10
|
+
"dev": "vitest run && ./build.sh"
|
|
11
|
+
},
|
|
12
|
+
"author": "Potch",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"onchange": "^7.1.0",
|
|
16
|
+
"terser": "^5.29.1",
|
|
17
|
+
"vitest": "^2.1.6"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/sizes.md
ADDED
package/src/munifw.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @potch/minifw/fw.js without so much code golfing
|
|
2
|
+
|
|
3
|
+
// prop to monitor for changes
|
|
4
|
+
const val = "value";
|
|
5
|
+
const isObj = (o) => typeof o === "object";
|
|
6
|
+
const map = (a, fn) => [...a].map(fn);
|
|
7
|
+
// utility to run a function and ensure cleanup after
|
|
8
|
+
export const using = (cb, fn) => (fn(), cb());
|
|
9
|
+
// collect event teardowns within the scope of a function
|
|
10
|
+
export const collect = (onEvent, fn, collection = []) => (
|
|
11
|
+
using(
|
|
12
|
+
onEvent((o) => collection.push(o)),
|
|
13
|
+
fn
|
|
14
|
+
),
|
|
15
|
+
collection
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// event primitive, returns [emit, watch] fns
|
|
19
|
+
export const event = (watchers = new Set()) => [
|
|
20
|
+
(...args) => map(watchers, (fn) => fn(...args)),
|
|
21
|
+
(fn) => {
|
|
22
|
+
watchers.add(fn);
|
|
23
|
+
return () => watchers.delete(fn);
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// used for computed/effect bookkeeping
|
|
28
|
+
const context = [];
|
|
29
|
+
|
|
30
|
+
// reactive value primitive, notifies when .value changes
|
|
31
|
+
export const signal = (value, eq = (a, b) => a === b) => {
|
|
32
|
+
// create event bus
|
|
33
|
+
const [emit, watch] = event();
|
|
34
|
+
return {
|
|
35
|
+
set [val](v) {
|
|
36
|
+
if (!eq(value, v)) {
|
|
37
|
+
value = v;
|
|
38
|
+
emit();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
// manually trigger a value update
|
|
42
|
+
touch() {
|
|
43
|
+
emit();
|
|
44
|
+
},
|
|
45
|
+
get [val]() {
|
|
46
|
+
// if we're in an effect callback, register as a dep
|
|
47
|
+
if (context[0]) {
|
|
48
|
+
context[0].add(this);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
},
|
|
52
|
+
peek: () => value,
|
|
53
|
+
watch,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// global event bus for effects
|
|
58
|
+
export const [emitEffect, onEffect] = event();
|
|
59
|
+
|
|
60
|
+
// pure side effect
|
|
61
|
+
// runs when any signal referenced in `fn` by `.value` changes
|
|
62
|
+
// use signal.peek in effects to avoid dependency tracking
|
|
63
|
+
export const effect = (fn, ...explicitDependencies) => {
|
|
64
|
+
let inUpdate = false;
|
|
65
|
+
|
|
66
|
+
context.push(new Set(explicitDependencies));
|
|
67
|
+
|
|
68
|
+
const update = () => {
|
|
69
|
+
// prevents effects effecting themselves
|
|
70
|
+
if (!inUpdate) {
|
|
71
|
+
inUpdate = true;
|
|
72
|
+
fn((...args) => emitEffect(effect(...args)));
|
|
73
|
+
inUpdate = false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
update();
|
|
78
|
+
|
|
79
|
+
const teardown = map(context.at(-1), (dependency) =>
|
|
80
|
+
dependency.watch(update)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
context.pop();
|
|
84
|
+
|
|
85
|
+
return () => map(teardown, (fn) => fn());
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// derived reactive value, composition of a signal and an effect
|
|
89
|
+
// auto updates when any signal referenced in `fn` by `.value` changes
|
|
90
|
+
export const computed = (fn, ...explicitDependencies) => {
|
|
91
|
+
const out = signal();
|
|
92
|
+
out.teardown = effect(
|
|
93
|
+
() => (out[val] = fn(out[val])),
|
|
94
|
+
...explicitDependencies
|
|
95
|
+
);
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// credit to @developit/preact for logic here
|
|
100
|
+
export const setProp = (el, key, value) => {
|
|
101
|
+
if (key == "ref" && val in value) {
|
|
102
|
+
// if key is "ref" and value is signal-like, treat value as signal and set el as value
|
|
103
|
+
value[val] = el;
|
|
104
|
+
} else if (isObj(value) && val in value) {
|
|
105
|
+
// if value is signal-like, mount an effect to update prop
|
|
106
|
+
emitEffect(effect(() => setProp(el, key, value[val])));
|
|
107
|
+
} else if (typeof value === "function" || isObj(value) || key in el) {
|
|
108
|
+
// is value a function, object, or is the key an extant prop?
|
|
109
|
+
// situations where we always set prop
|
|
110
|
+
// if el[key] is object, deep merge it, else set it.
|
|
111
|
+
if (el[key] && isObj(el[key])) {
|
|
112
|
+
assign(el[key], value);
|
|
113
|
+
} else {
|
|
114
|
+
el[key] = value === null ? "" : value;
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// treat as attribute
|
|
118
|
+
// set if value not null and either not false or prop is `data-` or `aria-`
|
|
119
|
+
if (
|
|
120
|
+
value !== null &&
|
|
121
|
+
(value !== false || key.startsWith("data-") || key.startsWith("aria-"))
|
|
122
|
+
) {
|
|
123
|
+
el.setAttribute(key, value);
|
|
124
|
+
} else {
|
|
125
|
+
el.removeAttribute(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// deep merge assignment
|
|
131
|
+
export const assign = (a, b) => {
|
|
132
|
+
if (b) {
|
|
133
|
+
Object.entries(b).forEach(([key, value]) => {
|
|
134
|
+
if (a.nodeType) {
|
|
135
|
+
// check if a is an HTMLElement (duck type for SSR)
|
|
136
|
+
setProp(a, key, value);
|
|
137
|
+
} else if (isObj(a[key]) && isObj(value)) {
|
|
138
|
+
// if both current and new value are objects, recursively deep merge
|
|
139
|
+
assign(a[key], value);
|
|
140
|
+
} else {
|
|
141
|
+
a[key] = value;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return a;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// create DOM, hyperscript compatible
|
|
149
|
+
export const dom = (tag, props, ...children) => {
|
|
150
|
+
let el;
|
|
151
|
+
if (typeof tag === "function") {
|
|
152
|
+
el = mount(() => tag(props, children));
|
|
153
|
+
} else {
|
|
154
|
+
el = document.createElement(tag);
|
|
155
|
+
// allow optional props syntax
|
|
156
|
+
if (props && isObj(props) && !props.nodeType) {
|
|
157
|
+
assign(el, props);
|
|
158
|
+
} else {
|
|
159
|
+
if (props) {
|
|
160
|
+
children.unshift(props);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
el.append(...children.flat(1));
|
|
164
|
+
}
|
|
165
|
+
return el;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// a "computed" value that can mount and update in DOM
|
|
169
|
+
export const mount = (fn, ...explicitDependencies) => {
|
|
170
|
+
// track effects for disconnect
|
|
171
|
+
const teardowns = [];
|
|
172
|
+
let currentEl;
|
|
173
|
+
emitEffect(
|
|
174
|
+
effect(() => {
|
|
175
|
+
// disconnect existing effects
|
|
176
|
+
while (teardowns.length) {
|
|
177
|
+
try {
|
|
178
|
+
teardowns.pop()();
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
}
|
|
181
|
+
// track any effects created when generating mounted DOM
|
|
182
|
+
collect(
|
|
183
|
+
onEffect,
|
|
184
|
+
() => {
|
|
185
|
+
const newEl = fn();
|
|
186
|
+
if (currentEl) {
|
|
187
|
+
// swap or remove new element
|
|
188
|
+
if (newEl) {
|
|
189
|
+
currentEl.replaceWith(newEl);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
currentEl = newEl;
|
|
193
|
+
},
|
|
194
|
+
teardowns
|
|
195
|
+
);
|
|
196
|
+
}, ...explicitDependencies)
|
|
197
|
+
);
|
|
198
|
+
return currentEl;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// dom event listeners, returns a callback to un-listen
|
|
202
|
+
export const on = (target, ...args) => {
|
|
203
|
+
target.addEventListener(...args);
|
|
204
|
+
return () => target.removeEventListener(...args);
|
|
205
|
+
};
|
package/src/ssr.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// fake enough DOM for the `dom()` function to work
|
|
2
|
+
// not even a little bit standards compliant
|
|
3
|
+
|
|
4
|
+
const voids =
|
|
5
|
+
/^(area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/;
|
|
6
|
+
|
|
7
|
+
const attributes = "attributes";
|
|
8
|
+
const pn = "parentNode";
|
|
9
|
+
const cn = "childNodes";
|
|
10
|
+
const obj = Object;
|
|
11
|
+
const mapCn = (el, fn) => el[cn].map(fn);
|
|
12
|
+
|
|
13
|
+
const nodeMock = {
|
|
14
|
+
append(...nodes) {
|
|
15
|
+
nodes.map((n) => {
|
|
16
|
+
n = n?.nodeType ? n : _node(3, { textContent: "" + n });
|
|
17
|
+
this[cn].push(n);
|
|
18
|
+
n[pn] = this;
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
replaceWith(node) {
|
|
22
|
+
const parentNode = this[pn];
|
|
23
|
+
if (parentNode) {
|
|
24
|
+
parentNode[cn] = mapCn(parentNode, (n) => (n == this ? node : n));
|
|
25
|
+
node[pn] = parentNode;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
replaceChildren(...nodes) {
|
|
29
|
+
mapCn(this, (n) => n.remove());
|
|
30
|
+
this.append(...nodes);
|
|
31
|
+
},
|
|
32
|
+
setAttribute(a, v) {
|
|
33
|
+
this[attributes][a] = v;
|
|
34
|
+
},
|
|
35
|
+
getAttribute(a) {
|
|
36
|
+
return this[attributes][a];
|
|
37
|
+
},
|
|
38
|
+
removeAttribute(a) {
|
|
39
|
+
delete this[attributes][a];
|
|
40
|
+
},
|
|
41
|
+
remove() {
|
|
42
|
+
const self = this;
|
|
43
|
+
const parentNode = self[pn];
|
|
44
|
+
if (parentNode) {
|
|
45
|
+
parentNode[cn] = parentNode[cn].filter((n) => n != self);
|
|
46
|
+
self[pn] = null;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
get outerHTML() {
|
|
50
|
+
const self = this;
|
|
51
|
+
const { tagName, nodeType } = self;
|
|
52
|
+
if (nodeType == 1) {
|
|
53
|
+
return (
|
|
54
|
+
"<" +
|
|
55
|
+
tagName +
|
|
56
|
+
obj
|
|
57
|
+
.entries(self[attributes])
|
|
58
|
+
.map((a) => ` ${a[0]}="${a[1]}"`)
|
|
59
|
+
.join("") +
|
|
60
|
+
">" +
|
|
61
|
+
self.innerHTML +
|
|
62
|
+
(voids.test(tagName) ? "" : `</${tagName}>`)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (nodeType == 3) {
|
|
66
|
+
return self.textContent;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
},
|
|
70
|
+
get innerHTML() {
|
|
71
|
+
return mapCn(this, (n) => n.outerHTML).join("");
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const _node = (nodeType, props) =>
|
|
76
|
+
obj.assign(obj.create(nodeMock), { nodeType }, props);
|
|
77
|
+
|
|
78
|
+
const createElement = (tagName) =>
|
|
79
|
+
_node(1, {
|
|
80
|
+
tagName: tagName.toLowerCase(),
|
|
81
|
+
[attributes]: {},
|
|
82
|
+
[cn]: [],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export default {
|
|
86
|
+
createElement,
|
|
87
|
+
// used by ssr instead of getElementById
|
|
88
|
+
find: (el, id) => {
|
|
89
|
+
let current,
|
|
90
|
+
stack = [el];
|
|
91
|
+
do {
|
|
92
|
+
current = stack.pop();
|
|
93
|
+
if (current?.[attributes]?.id == id) return current;
|
|
94
|
+
if (current) {
|
|
95
|
+
stack.push(...current[cn]);
|
|
96
|
+
}
|
|
97
|
+
} while (stack.length);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
assign,
|
|
4
|
+
event,
|
|
5
|
+
signal,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
dom,
|
|
9
|
+
onEffect,
|
|
10
|
+
using,
|
|
11
|
+
collect,
|
|
12
|
+
} from "../src/munifw.js";
|
|
13
|
+
|
|
14
|
+
describe("fw", () => {
|
|
15
|
+
describe("assign", () => {
|
|
16
|
+
it("shallow", () => {
|
|
17
|
+
const o = assign({ a: 1 }, { b: 2 });
|
|
18
|
+
expect(o).toStrictEqual({ a: 1, b: 2 });
|
|
19
|
+
expect(assign({ a: 1 }, null)).toStrictEqual({ a: 1 });
|
|
20
|
+
});
|
|
21
|
+
it("deep", () => {
|
|
22
|
+
const o = assign({ a: { deep: 1 } }, { b: 2, a: { deeper: 2 } });
|
|
23
|
+
expect(o).toStrictEqual({ a: { deep: 1, deeper: 2 }, b: 2 });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("event", () => {
|
|
28
|
+
let watch, emit;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
[emit, watch] = event();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("emits", () => {
|
|
35
|
+
const fn = vi.fn();
|
|
36
|
+
watch(fn);
|
|
37
|
+
emit(1);
|
|
38
|
+
expect(fn).toHaveBeenCalledWith(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("maps emit", () => {
|
|
42
|
+
const fn = vi.fn();
|
|
43
|
+
watch((a) => a + 1);
|
|
44
|
+
watch((a) => a + 2);
|
|
45
|
+
const result = emit(1);
|
|
46
|
+
expect(result).toStrictEqual([2, 3]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("unwatches", () => {
|
|
50
|
+
const fn = vi.fn();
|
|
51
|
+
const unwatch = watch(fn);
|
|
52
|
+
emit(1);
|
|
53
|
+
expect(fn).toHaveBeenCalled();
|
|
54
|
+
unwatch();
|
|
55
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("signal", () => {
|
|
60
|
+
let s;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
s = signal(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("can get value", () => {
|
|
67
|
+
expect(s.value).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
it("can update value", () => {
|
|
70
|
+
expect(s.value).toBe(1);
|
|
71
|
+
s.value = 2;
|
|
72
|
+
expect(s.value).toBe(2);
|
|
73
|
+
});
|
|
74
|
+
it("watches", () => {
|
|
75
|
+
const fn = vi.fn();
|
|
76
|
+
s.watch(fn);
|
|
77
|
+
s.value = 2;
|
|
78
|
+
expect(fn).toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
it("peeks", () => {
|
|
81
|
+
expect(s.peek()).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
it("un-watches", () => {
|
|
84
|
+
const fn = vi.fn();
|
|
85
|
+
const unwatch = s.watch(fn);
|
|
86
|
+
unwatch();
|
|
87
|
+
s.value = 2;
|
|
88
|
+
expect(fn).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
it("compares", () => {
|
|
91
|
+
const fn = vi.fn();
|
|
92
|
+
const s = signal({ a: 1 }, (a, b) => a.a === b.a);
|
|
93
|
+
s.watch(fn);
|
|
94
|
+
s.value = { a: 2 };
|
|
95
|
+
expect(fn).toHaveBeenCalled();
|
|
96
|
+
s.value = { a: 2, b: 1 };
|
|
97
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("effect", () => {
|
|
102
|
+
let s1, s2;
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
s1 = signal(1);
|
|
105
|
+
s2 = signal(2);
|
|
106
|
+
});
|
|
107
|
+
it("runs once", () => {
|
|
108
|
+
const fn = vi.fn();
|
|
109
|
+
effect(() => fn(s1.value + s2.value));
|
|
110
|
+
expect(fn).toHaveBeenCalledWith(3);
|
|
111
|
+
});
|
|
112
|
+
it("runs on change", () => {
|
|
113
|
+
const fn = vi.fn();
|
|
114
|
+
effect(() => fn(s1.value + s2.value));
|
|
115
|
+
expect(fn).toHaveBeenCalledWith(3);
|
|
116
|
+
s1.value = 3;
|
|
117
|
+
expect(fn).toHaveBeenCalledWith(5);
|
|
118
|
+
});
|
|
119
|
+
it("peeks", () => {
|
|
120
|
+
const fn = vi.fn();
|
|
121
|
+
effect(() => fn(s1.peek() + s2.value));
|
|
122
|
+
expect(fn).toHaveBeenCalledWith(3);
|
|
123
|
+
s1.value = 3;
|
|
124
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
125
|
+
s2.value = 3;
|
|
126
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
127
|
+
});
|
|
128
|
+
it("un-watches", () => {
|
|
129
|
+
const fn = vi.fn();
|
|
130
|
+
const unwatch = effect(() => fn(s1.value + s2.value));
|
|
131
|
+
expect(fn).toHaveBeenCalledWith(3);
|
|
132
|
+
s1.value = 3;
|
|
133
|
+
expect(fn).toHaveBeenCalledWith(5);
|
|
134
|
+
unwatch();
|
|
135
|
+
s1.value = 6;
|
|
136
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
137
|
+
});
|
|
138
|
+
it("cycle protects", () => {
|
|
139
|
+
const fn = vi.fn();
|
|
140
|
+
effect(() => {
|
|
141
|
+
s1.value++;
|
|
142
|
+
fn(s1.value + s2.value);
|
|
143
|
+
});
|
|
144
|
+
s2.value++;
|
|
145
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
146
|
+
});
|
|
147
|
+
it("independent nested effects", () => {
|
|
148
|
+
const s1 = signal(1);
|
|
149
|
+
const s2 = signal(2);
|
|
150
|
+
const fn1 = vi.fn();
|
|
151
|
+
const fn2 = vi.fn();
|
|
152
|
+
|
|
153
|
+
effect(() => {
|
|
154
|
+
fn1();
|
|
155
|
+
effect(() => {
|
|
156
|
+
fn2();
|
|
157
|
+
}, s2);
|
|
158
|
+
}, s1);
|
|
159
|
+
|
|
160
|
+
s2.value = 3;
|
|
161
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(fn2).toHaveBeenCalledTimes(2);
|
|
163
|
+
});
|
|
164
|
+
it("inner effects", () => {
|
|
165
|
+
const fn = vi.fn();
|
|
166
|
+
const s2 = signal(1);
|
|
167
|
+
const innerEffects = collect(onEffect, () =>
|
|
168
|
+
effect((effect) => {
|
|
169
|
+
effect(fn, s2);
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
s2.value = 2;
|
|
173
|
+
while (innerEffects.length) innerEffects.pop()();
|
|
174
|
+
s2.value = 3;
|
|
175
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("computed", () => {
|
|
180
|
+
let s1, s2, c;
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
s1 = signal(1);
|
|
183
|
+
s2 = signal(2);
|
|
184
|
+
c = computed(() => s1.value + s2.value);
|
|
185
|
+
});
|
|
186
|
+
it("has value", () => {
|
|
187
|
+
expect(c.value).toBe(3);
|
|
188
|
+
});
|
|
189
|
+
it("updates value", () => {
|
|
190
|
+
s1.value = 3;
|
|
191
|
+
expect(c.value).toBe(5);
|
|
192
|
+
});
|
|
193
|
+
it("works with effects", () => {
|
|
194
|
+
const fn = vi.fn();
|
|
195
|
+
const s3 = signal(4);
|
|
196
|
+
effect(() => {
|
|
197
|
+
fn(c.value + s3.value);
|
|
198
|
+
});
|
|
199
|
+
expect(fn).toHaveBeenCalledWith(7);
|
|
200
|
+
});
|
|
201
|
+
it("watches", () => {
|
|
202
|
+
const fn = vi.fn();
|
|
203
|
+
c.watch(fn);
|
|
204
|
+
s1.value = 2;
|
|
205
|
+
expect(fn).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
it("peeks", () => {
|
|
208
|
+
expect(c.peek()).toBe(3);
|
|
209
|
+
});
|
|
210
|
+
it("un-watches", () => {
|
|
211
|
+
const fn = vi.fn();
|
|
212
|
+
const unwatch = c.watch(fn);
|
|
213
|
+
unwatch();
|
|
214
|
+
s1.value = 2;
|
|
215
|
+
expect(fn).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const child = dom("foo");
|
|
220
|
+
|
|
221
|
+
describe("dom", () => {
|
|
222
|
+
it("creates", () => {
|
|
223
|
+
const el = dom("div", {}, 2, 3, 4);
|
|
224
|
+
expect(document.createElement).toHaveBeenCalledWith("div");
|
|
225
|
+
expect(el.append).toHaveBeenCalledWith(2, 3, 4);
|
|
226
|
+
});
|
|
227
|
+
it("creates with optional props", () => {
|
|
228
|
+
const el = dom("div");
|
|
229
|
+
expect(document.createElement).toHaveBeenCalledWith("div");
|
|
230
|
+
});
|
|
231
|
+
it("creates with optional props and children", () => {
|
|
232
|
+
const el = dom("div", "hello", child);
|
|
233
|
+
expect(document.createElement).toHaveBeenCalledWith("div");
|
|
234
|
+
expect(el.append).toHaveBeenCalledWith("hello", child);
|
|
235
|
+
});
|
|
236
|
+
it("sets attributes", () => {
|
|
237
|
+
const fn = vi.fn();
|
|
238
|
+
const s = signal();
|
|
239
|
+
const el = dom("test", { "data-test": 1, a: fn, ref: s, o: { a: 1 } });
|
|
240
|
+
expect(document.createElement).toHaveBeenCalledWith("test");
|
|
241
|
+
expect(el.setAttribute).toHaveBeenCalledTimes(1);
|
|
242
|
+
expect(el.setAttribute).toHaveBeenCalledWith("data-test", 1);
|
|
243
|
+
expect(el.a).toBe(fn);
|
|
244
|
+
expect(s.value).toBe(el);
|
|
245
|
+
expect(el.o).toStrictEqual({ a: 1 });
|
|
246
|
+
});
|
|
247
|
+
it("binds signals", () => {
|
|
248
|
+
const s = signal("foo");
|
|
249
|
+
const el = dom("test", { id: s });
|
|
250
|
+
expect(document.createElement).toHaveBeenCalledWith("test");
|
|
251
|
+
expect(el.setAttribute).toHaveBeenCalledTimes(1);
|
|
252
|
+
expect(el.setAttribute).toHaveBeenCalledWith("id", "foo");
|
|
253
|
+
s.value = "bar";
|
|
254
|
+
expect(el.setAttribute).toHaveBeenCalledTimes(2);
|
|
255
|
+
expect(el.setAttribute).toHaveBeenCalledWith("id", "bar");
|
|
256
|
+
s.value = false;
|
|
257
|
+
expect(el.removeAttribute).toHaveBeenCalledTimes(1);
|
|
258
|
+
expect(el.removeAttribute).toHaveBeenCalledWith("id");
|
|
259
|
+
});
|
|
260
|
+
it("captures effects", () => {
|
|
261
|
+
const fn = vi.fn();
|
|
262
|
+
const s = signal("foo");
|
|
263
|
+
const s2 = signal("bar");
|
|
264
|
+
onEffect(fn);
|
|
265
|
+
const el = dom("test", { id: s, name: s2 });
|
|
266
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("utils", () => {
|
|
271
|
+
it("using", () => {
|
|
272
|
+
const fn = vi.fn();
|
|
273
|
+
const cb = vi.fn();
|
|
274
|
+
using(cb, () => {
|
|
275
|
+
fn();
|
|
276
|
+
});
|
|
277
|
+
expect(fn).toHaveBeenCalled();
|
|
278
|
+
expect(cb).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
it("collect", () => {
|
|
281
|
+
const [emit, on] = event();
|
|
282
|
+
const collected = collect(on, () => {
|
|
283
|
+
for (let i = 1; i <= 3; i++) {
|
|
284
|
+
emit(i);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
expect(collected).toStrictEqual([1, 2, 3]);
|
|
288
|
+
emit(4);
|
|
289
|
+
expect(collected).toStrictEqual([1, 2, 3]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
package/test/setup.js
ADDED
package/test/ssr.test.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ssr from "../src/ssr.js";
|
|
2
|
+
vi.stubGlobal("document", ssr);
|
|
3
|
+
import { dom } from "../src/munifw.js";
|
|
4
|
+
|
|
5
|
+
describe("createElement", () => {
|
|
6
|
+
it("makes tags", () => {
|
|
7
|
+
const el = ssr.createElement("div");
|
|
8
|
+
expect(el.tagName).toBe("div");
|
|
9
|
+
});
|
|
10
|
+
it("makes text", () => {
|
|
11
|
+
const el = ssr.createElement("div");
|
|
12
|
+
el.append("foo");
|
|
13
|
+
const text = el.childNodes[0];
|
|
14
|
+
expect(text.nodeType).toBe(3);
|
|
15
|
+
expect(text.textContent).toBe("foo");
|
|
16
|
+
});
|
|
17
|
+
it("works with muni", () => {
|
|
18
|
+
const spy = vi.spyOn(ssr, "createElement");
|
|
19
|
+
const el = dom("div");
|
|
20
|
+
expect(spy).toHaveBeenCalled();
|
|
21
|
+
expect(el.tagName).toBe("div");
|
|
22
|
+
expect(el.nodeType).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("nodeMock", () => {
|
|
27
|
+
it("attributes", () => {
|
|
28
|
+
const el = ssr.createElement("div");
|
|
29
|
+
el.setAttribute("foo", "bar");
|
|
30
|
+
expect(el.getAttribute("foo")).toBe("bar");
|
|
31
|
+
el.removeAttribute("foo");
|
|
32
|
+
expect(el.getAttribute("foo")).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
it("children", () => {
|
|
35
|
+
const el = ssr.createElement("div");
|
|
36
|
+
const h1 = ssr.createElement("h1");
|
|
37
|
+
el.append(h1);
|
|
38
|
+
expect(el.childNodes[0]).toBe(h1);
|
|
39
|
+
expect(h1.parentNode).toBe(el);
|
|
40
|
+
const h2 = ssr.createElement("h2");
|
|
41
|
+
el.replaceChildren(h2);
|
|
42
|
+
expect(el.childNodes.length).toBe(1);
|
|
43
|
+
expect(el.childNodes[0]).toBe(h2);
|
|
44
|
+
expect(h1.parentNode).toBeNull();
|
|
45
|
+
el.append(h1);
|
|
46
|
+
expect(el.childNodes.length).toBe(2);
|
|
47
|
+
h2.remove();
|
|
48
|
+
expect(el.childNodes[0]).toBe(h1);
|
|
49
|
+
h1.replaceWith(h2);
|
|
50
|
+
expect(el.childNodes[0]).toBe(h2);
|
|
51
|
+
});
|
|
52
|
+
it("serializes", () => {
|
|
53
|
+
const el = ssr.createElement("div");
|
|
54
|
+
const a = ssr.createElement("a");
|
|
55
|
+
a.setAttribute("href", "foo");
|
|
56
|
+
a.append("bar");
|
|
57
|
+
el.append(a);
|
|
58
|
+
el.append(ssr.createElement("img"));
|
|
59
|
+
el.append(ssr.createElement("b"));
|
|
60
|
+
expect(el.outerHTML).toBe('<div><a href="foo">bar</a><img><b></b></div>');
|
|
61
|
+
expect(el.innerHTML).toBe('<a href="foo">bar</a><img><b></b>');
|
|
62
|
+
expect(a.innerHTML).toBe("bar");
|
|
63
|
+
});
|
|
64
|
+
});
|