cajoa 0.1.0 → 0.2.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 +73 -18
- package/dist/index.cjs +114 -14
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +108 -13
- package/package.json +57 -57
package/README.md
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
A small, beginner-friendly frontend framework built on **signals**.
|
|
4
4
|
No virtual DOM, no hooks rules, no re-rendering components.
|
|
5
5
|
|
|
6
|
-
> ⚠️ **v0.
|
|
6
|
+
> ⚠️ **v0.2 (early stage).** The API may still change. Not production-ready yet.
|
|
7
7
|
|
|
8
8
|
## Why Cajoa?
|
|
9
9
|
|
|
10
10
|
- **Components run once.** A component is just a function that builds DOM. Only the parts that depend on a signal update.
|
|
11
|
-
- **Tiny and readable.** The whole core is
|
|
11
|
+
- **Tiny and readable.** The whole core is small enough to read in one sitting.
|
|
12
12
|
- **TypeScript-first**, ESM + CommonJS, zero runtime dependencies.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
@@ -39,26 +39,80 @@ render(Counter, document.getElementById("app")!);
|
|
|
39
39
|
|
|
40
40
|
Passing `count` (the function itself, not `count()`) is what makes that part of the DOM reactive.
|
|
41
41
|
|
|
42
|
+
## Lists and conditions
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createSignal, h, Show, For } from "cajoa";
|
|
46
|
+
|
|
47
|
+
const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
48
|
+
|
|
49
|
+
h(
|
|
50
|
+
"div",
|
|
51
|
+
null,
|
|
52
|
+
Show({
|
|
53
|
+
when: () => todos().length > 0,
|
|
54
|
+
fallback: () => h("p", null, "Nothing to do."),
|
|
55
|
+
children: () =>
|
|
56
|
+
h(
|
|
57
|
+
"ul",
|
|
58
|
+
null,
|
|
59
|
+
For({
|
|
60
|
+
each: todos,
|
|
61
|
+
children: (todo) => h("li", null, todo.text),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
42
68
|
## API
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
### Reactivity
|
|
71
|
+
|
|
72
|
+
| Function | What it does |
|
|
73
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `createSignal(value)` | Returns `[get, set]`. Reading `get()` inside an effect subscribes to it. `set` accepts a value or an updater function. |
|
|
75
|
+
| `createEffect(fn)` | Runs `fn` now and again whenever a signal it read changes. |
|
|
76
|
+
| `createMemo(fn)` | Cached computed value. Only notifies when the result changes. |
|
|
77
|
+
| `batch(fn)` | Groups several `set` calls into one update. |
|
|
78
|
+
|
|
79
|
+
### DOM
|
|
80
|
+
|
|
81
|
+
| Function | What it does |
|
|
82
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
83
|
+
| `h(tag, props, ...children)` | Creates a real DOM element. Function props/children are reactive. |
|
|
84
|
+
| `render(component, container)` | Mounts a component. Returns a `dispose` function. |
|
|
85
|
+
| `Show({ when, children, fallback })` | Conditional rendering. |
|
|
86
|
+
| `For({ each, children, key? })` | List rendering. Reuses DOM nodes for unchanged items. `children` must return a `Node`. |
|
|
87
|
+
|
|
88
|
+
### Lifecycle
|
|
89
|
+
|
|
90
|
+
| Function | What it does |
|
|
91
|
+
| ---------------- | ----------------------------------------------------------------- |
|
|
92
|
+
| `onMount(fn)` | Runs `fn` after the component is rendered. |
|
|
93
|
+
| `onCleanup(fn)` | Runs `fn` before an effect re-runs or when its owner is disposed. |
|
|
94
|
+
| `createRoot(fn)` | Creates a top-level owner you can dispose manually. |
|
|
95
|
+
|
|
96
|
+
### Context
|
|
97
|
+
|
|
98
|
+
| Function | What it does |
|
|
99
|
+
| ------------------------- | ------------------------------------------------------ |
|
|
100
|
+
| `createContext(default)` | Creates a context with a default value. |
|
|
101
|
+
| `provide(ctx, value, fn)` | Runs `fn`; everything created inside can read `value`. |
|
|
102
|
+
| `useContext(ctx)` | Reads the nearest provided value (or the default). |
|
|
103
|
+
|
|
104
|
+
## Good to know
|
|
105
|
+
|
|
106
|
+
- `For` uses the item itself as the key by default. If you pass `key`, items with the same key are **not** re-rendered when their data changes, so keys must be unique and the item should hold its own signals if it needs to update.
|
|
107
|
+
- `useContext` only works while a component is being created (or inside `onMount` / effects). Read it at the top of your component and use the value later in handlers.
|
|
108
|
+
- Updating an object in place will not trigger effects. Set a new object instead.
|
|
55
109
|
|
|
56
110
|
## Known limitations
|
|
57
111
|
|
|
58
|
-
- `For` rebuilds the whole list on every change (keyed diffing is planned).
|
|
59
|
-
- No `createMemo`, `batch`, or context yet.
|
|
60
112
|
- No JSX support yet (`h()` only).
|
|
61
113
|
- No SSR.
|
|
114
|
+
- `createMemo` can show a brief inconsistent state when memos depend on each other.
|
|
115
|
+
- `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
|
|
62
116
|
|
|
63
117
|
## Roadmap
|
|
64
118
|
|
|
@@ -66,11 +120,12 @@ Passing `count` (the function itself, not `count()`) is what makes that part of
|
|
|
66
120
|
- [x] DOM bindings (`h`, `render`)
|
|
67
121
|
- [x] `Show`, `For`
|
|
68
122
|
- [x] Lifecycle (`onMount`, `onCleanup`) and disposal
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
123
|
+
- [x] `createMemo`, `batch`
|
|
124
|
+
- [x] Keyed `For`
|
|
125
|
+
- [x] Context
|
|
72
126
|
- [ ] JSX support
|
|
73
127
|
- [ ] Router
|
|
128
|
+
- [ ] SSR
|
|
74
129
|
|
|
75
130
|
## Development
|
|
76
131
|
|
package/dist/index.cjs
CHANGED
|
@@ -22,13 +22,18 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
For: () => For,
|
|
24
24
|
Show: () => Show,
|
|
25
|
+
batch: () => batch,
|
|
26
|
+
createContext: () => createContext,
|
|
25
27
|
createEffect: () => createEffect,
|
|
28
|
+
createMemo: () => createMemo,
|
|
26
29
|
createRoot: () => createRoot,
|
|
27
30
|
createSignal: () => createSignal,
|
|
28
31
|
h: () => h,
|
|
29
32
|
onCleanup: () => onCleanup,
|
|
30
33
|
onMount: () => onMount,
|
|
31
|
-
|
|
34
|
+
provide: () => provide,
|
|
35
|
+
render: () => render,
|
|
36
|
+
useContext: () => useContext
|
|
32
37
|
});
|
|
33
38
|
module.exports = __toCommonJS(index_exports);
|
|
34
39
|
|
|
@@ -45,6 +50,22 @@ function cleanComputation(c) {
|
|
|
45
50
|
c.deps.clear();
|
|
46
51
|
clean(c);
|
|
47
52
|
}
|
|
53
|
+
var batchDepth = 0;
|
|
54
|
+
var pending = /* @__PURE__ */ new Set();
|
|
55
|
+
function flush() {
|
|
56
|
+
const effects = [...pending];
|
|
57
|
+
pending.clear();
|
|
58
|
+
effects.forEach((effect) => effect.run());
|
|
59
|
+
}
|
|
60
|
+
function batch(fn) {
|
|
61
|
+
batchDepth++;
|
|
62
|
+
try {
|
|
63
|
+
return fn();
|
|
64
|
+
} finally {
|
|
65
|
+
batchDepth--;
|
|
66
|
+
if (batchDepth === 0) flush();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
48
69
|
function createSignal(value) {
|
|
49
70
|
const subs = /* @__PURE__ */ new Set();
|
|
50
71
|
const get = () => {
|
|
@@ -58,13 +79,18 @@ function createSignal(value) {
|
|
|
58
79
|
const v = typeof next === "function" ? next(value) : next;
|
|
59
80
|
if (Object.is(v, value)) return;
|
|
60
81
|
value = v;
|
|
61
|
-
[...subs]
|
|
82
|
+
for (const effect of [...subs]) {
|
|
83
|
+
if (batchDepth > 0)
|
|
84
|
+
pending.add(effect);
|
|
85
|
+
else effect.run();
|
|
86
|
+
}
|
|
62
87
|
};
|
|
63
88
|
return [get, set];
|
|
64
89
|
}
|
|
65
90
|
function createEffect(fn) {
|
|
66
91
|
const effect = {
|
|
67
92
|
cleanups: [],
|
|
93
|
+
parent: currentOwner,
|
|
68
94
|
deps: /* @__PURE__ */ new Set(),
|
|
69
95
|
disposed: false,
|
|
70
96
|
run() {
|
|
@@ -92,7 +118,7 @@ function onCleanup(fn) {
|
|
|
92
118
|
currentOwner?.cleanups.push(fn);
|
|
93
119
|
}
|
|
94
120
|
function createRoot(fn) {
|
|
95
|
-
const root = { cleanups: [] };
|
|
121
|
+
const root = { cleanups: [], parent: currentOwner };
|
|
96
122
|
const prevOwner = currentOwner;
|
|
97
123
|
const prevEffect = currentEffect;
|
|
98
124
|
currentOwner = root;
|
|
@@ -119,6 +145,30 @@ function onMount(fn) {
|
|
|
119
145
|
}
|
|
120
146
|
});
|
|
121
147
|
}
|
|
148
|
+
function createContext(defaultValue) {
|
|
149
|
+
return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
|
|
150
|
+
}
|
|
151
|
+
function provide(ctx, value, fn) {
|
|
152
|
+
const owner = {
|
|
153
|
+
cleanups: [],
|
|
154
|
+
parent: currentOwner,
|
|
155
|
+
context: /* @__PURE__ */ new Map([[ctx.id, value]])
|
|
156
|
+
};
|
|
157
|
+
onCleanup(() => clean(owner));
|
|
158
|
+
const prev = currentOwner;
|
|
159
|
+
currentOwner = owner;
|
|
160
|
+
try {
|
|
161
|
+
return fn();
|
|
162
|
+
} finally {
|
|
163
|
+
currentOwner = prev;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function useContext(ctx) {
|
|
167
|
+
for (let o = currentOwner; o; o = o.parent) {
|
|
168
|
+
if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
|
|
169
|
+
}
|
|
170
|
+
return ctx.defaultValue;
|
|
171
|
+
}
|
|
122
172
|
|
|
123
173
|
// src/dom/h.ts
|
|
124
174
|
function h(tag, props, ...children) {
|
|
@@ -152,12 +202,12 @@ function toNodes(value) {
|
|
|
152
202
|
return out;
|
|
153
203
|
}
|
|
154
204
|
function appendChildren(parent, children) {
|
|
155
|
-
const
|
|
156
|
-
while (
|
|
157
|
-
const child =
|
|
205
|
+
const pending2 = [...children].reverse();
|
|
206
|
+
while (pending2.length > 0) {
|
|
207
|
+
const child = pending2.pop();
|
|
158
208
|
if (Array.isArray(child)) {
|
|
159
209
|
for (let i = child.length - 1; i >= 0; i--) {
|
|
160
|
-
|
|
210
|
+
pending2.push(child[i]);
|
|
161
211
|
}
|
|
162
212
|
continue;
|
|
163
213
|
}
|
|
@@ -166,17 +216,29 @@ function appendChildren(parent, children) {
|
|
|
166
216
|
const end = document.createComment("");
|
|
167
217
|
parent.append(start, end);
|
|
168
218
|
createEffect(() => {
|
|
169
|
-
|
|
170
|
-
while (start.nextSibling && start.nextSibling !== end) {
|
|
171
|
-
start.nextSibling.remove();
|
|
172
|
-
}
|
|
173
|
-
end.before(...nodes);
|
|
219
|
+
reconcile(start, end, toNodes(child()));
|
|
174
220
|
});
|
|
175
221
|
} else {
|
|
176
222
|
parent.append(...toNodes(child));
|
|
177
223
|
}
|
|
178
224
|
}
|
|
179
225
|
}
|
|
226
|
+
function reconcile(start, end, next) {
|
|
227
|
+
const keep = new Set(next);
|
|
228
|
+
let cur = start.nextSibling;
|
|
229
|
+
while (cur && cur !== end) {
|
|
230
|
+
const after = cur.nextSibling;
|
|
231
|
+
if (!keep.has(cur)) cur.remove();
|
|
232
|
+
cur = after;
|
|
233
|
+
}
|
|
234
|
+
const parent = end.parentNode;
|
|
235
|
+
let ref = start.nextSibling;
|
|
236
|
+
for (const node of next) {
|
|
237
|
+
if (node === ref)
|
|
238
|
+
ref = ref.nextSibling;
|
|
239
|
+
else parent.insertBefore(node, ref);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
180
242
|
|
|
181
243
|
// src/dom/render.ts
|
|
182
244
|
function render(component, container) {
|
|
@@ -196,17 +258,55 @@ function Show(props) {
|
|
|
196
258
|
|
|
197
259
|
// src/components/for.ts
|
|
198
260
|
function For(props) {
|
|
199
|
-
|
|
261
|
+
let cache = /* @__PURE__ */ new Map();
|
|
262
|
+
onCleanup(() => {
|
|
263
|
+
cache.forEach((e) => e.dispose());
|
|
264
|
+
cache.clear();
|
|
265
|
+
});
|
|
266
|
+
return () => {
|
|
267
|
+
const items = props.each();
|
|
268
|
+
const next = /* @__PURE__ */ new Map();
|
|
269
|
+
const nodes = [];
|
|
270
|
+
items.forEach((item, i) => {
|
|
271
|
+
const k = props.key ? props.key(item, i) : item;
|
|
272
|
+
let entry = cache.get(k);
|
|
273
|
+
if (!entry) {
|
|
274
|
+
entry = createRoot((dispose) => ({
|
|
275
|
+
node: props.children(item, i),
|
|
276
|
+
dispose
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
next.set(k, entry);
|
|
280
|
+
nodes.push(entry.node);
|
|
281
|
+
});
|
|
282
|
+
cache.forEach((entry, k) => {
|
|
283
|
+
if (!next.has(k)) entry.dispose();
|
|
284
|
+
});
|
|
285
|
+
cache = next;
|
|
286
|
+
return nodes;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/reactivity/memo.ts
|
|
291
|
+
function createMemo(fn) {
|
|
292
|
+
const [value, setValue] = createSignal(void 0);
|
|
293
|
+
createEffect(() => setValue(() => fn()));
|
|
294
|
+
return value;
|
|
200
295
|
}
|
|
201
296
|
// Annotate the CommonJS export names for ESM import in node:
|
|
202
297
|
0 && (module.exports = {
|
|
203
298
|
For,
|
|
204
299
|
Show,
|
|
300
|
+
batch,
|
|
301
|
+
createContext,
|
|
205
302
|
createEffect,
|
|
303
|
+
createMemo,
|
|
206
304
|
createRoot,
|
|
207
305
|
createSignal,
|
|
208
306
|
h,
|
|
209
307
|
onCleanup,
|
|
210
308
|
onMount,
|
|
211
|
-
|
|
309
|
+
provide,
|
|
310
|
+
render,
|
|
311
|
+
useContext
|
|
212
312
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
type Cleanup = () => void;
|
|
2
|
+
declare function batch<T>(fn: () => T): T;
|
|
2
3
|
declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
|
|
3
4
|
declare function createEffect(fn: () => void): void;
|
|
4
5
|
declare function onCleanup(fn: Cleanup): void;
|
|
5
6
|
declare function createRoot<T>(fn: (dispose: () => void) => T): T;
|
|
6
7
|
declare function onMount(fn: () => void): void;
|
|
8
|
+
interface Context<T> {
|
|
9
|
+
readonly id: symbol;
|
|
10
|
+
readonly defaultValue: T;
|
|
11
|
+
}
|
|
12
|
+
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
|
+
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
|
+
declare function useContext<T>(ctx: Context<T>): T;
|
|
7
15
|
|
|
8
16
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
9
17
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -18,8 +26,11 @@ declare function Show<T>(props: {
|
|
|
18
26
|
|
|
19
27
|
declare function For<T>(props: {
|
|
20
28
|
each: () => T[];
|
|
21
|
-
children: (item: T, index: number) =>
|
|
22
|
-
|
|
29
|
+
children: (item: T, index: number) => Node;
|
|
30
|
+
key?: (item: T, index: number) => unknown;
|
|
31
|
+
}): () => Node[];
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
|
+
|
|
35
|
+
export { For, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, onCleanup, onMount, provide, render, useContext };
|
|
36
|
+
export type { Child, Context };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
type Cleanup = () => void;
|
|
2
|
+
declare function batch<T>(fn: () => T): T;
|
|
2
3
|
declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
|
|
3
4
|
declare function createEffect(fn: () => void): void;
|
|
4
5
|
declare function onCleanup(fn: Cleanup): void;
|
|
5
6
|
declare function createRoot<T>(fn: (dispose: () => void) => T): T;
|
|
6
7
|
declare function onMount(fn: () => void): void;
|
|
8
|
+
interface Context<T> {
|
|
9
|
+
readonly id: symbol;
|
|
10
|
+
readonly defaultValue: T;
|
|
11
|
+
}
|
|
12
|
+
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
|
+
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
|
+
declare function useContext<T>(ctx: Context<T>): T;
|
|
7
15
|
|
|
8
16
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
9
17
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -18,8 +26,11 @@ declare function Show<T>(props: {
|
|
|
18
26
|
|
|
19
27
|
declare function For<T>(props: {
|
|
20
28
|
each: () => T[];
|
|
21
|
-
children: (item: T, index: number) =>
|
|
22
|
-
|
|
29
|
+
children: (item: T, index: number) => Node;
|
|
30
|
+
key?: (item: T, index: number) => unknown;
|
|
31
|
+
}): () => Node[];
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
|
+
|
|
35
|
+
export { For, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, onCleanup, onMount, provide, render, useContext };
|
|
36
|
+
export type { Child, Context };
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,22 @@ function cleanComputation(c) {
|
|
|
11
11
|
c.deps.clear();
|
|
12
12
|
clean(c);
|
|
13
13
|
}
|
|
14
|
+
var batchDepth = 0;
|
|
15
|
+
var pending = /* @__PURE__ */ new Set();
|
|
16
|
+
function flush() {
|
|
17
|
+
const effects = [...pending];
|
|
18
|
+
pending.clear();
|
|
19
|
+
effects.forEach((effect) => effect.run());
|
|
20
|
+
}
|
|
21
|
+
function batch(fn) {
|
|
22
|
+
batchDepth++;
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
} finally {
|
|
26
|
+
batchDepth--;
|
|
27
|
+
if (batchDepth === 0) flush();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
function createSignal(value) {
|
|
15
31
|
const subs = /* @__PURE__ */ new Set();
|
|
16
32
|
const get = () => {
|
|
@@ -24,13 +40,18 @@ function createSignal(value) {
|
|
|
24
40
|
const v = typeof next === "function" ? next(value) : next;
|
|
25
41
|
if (Object.is(v, value)) return;
|
|
26
42
|
value = v;
|
|
27
|
-
[...subs]
|
|
43
|
+
for (const effect of [...subs]) {
|
|
44
|
+
if (batchDepth > 0)
|
|
45
|
+
pending.add(effect);
|
|
46
|
+
else effect.run();
|
|
47
|
+
}
|
|
28
48
|
};
|
|
29
49
|
return [get, set];
|
|
30
50
|
}
|
|
31
51
|
function createEffect(fn) {
|
|
32
52
|
const effect = {
|
|
33
53
|
cleanups: [],
|
|
54
|
+
parent: currentOwner,
|
|
34
55
|
deps: /* @__PURE__ */ new Set(),
|
|
35
56
|
disposed: false,
|
|
36
57
|
run() {
|
|
@@ -58,7 +79,7 @@ function onCleanup(fn) {
|
|
|
58
79
|
currentOwner?.cleanups.push(fn);
|
|
59
80
|
}
|
|
60
81
|
function createRoot(fn) {
|
|
61
|
-
const root = { cleanups: [] };
|
|
82
|
+
const root = { cleanups: [], parent: currentOwner };
|
|
62
83
|
const prevOwner = currentOwner;
|
|
63
84
|
const prevEffect = currentEffect;
|
|
64
85
|
currentOwner = root;
|
|
@@ -85,6 +106,30 @@ function onMount(fn) {
|
|
|
85
106
|
}
|
|
86
107
|
});
|
|
87
108
|
}
|
|
109
|
+
function createContext(defaultValue) {
|
|
110
|
+
return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
|
|
111
|
+
}
|
|
112
|
+
function provide(ctx, value, fn) {
|
|
113
|
+
const owner = {
|
|
114
|
+
cleanups: [],
|
|
115
|
+
parent: currentOwner,
|
|
116
|
+
context: /* @__PURE__ */ new Map([[ctx.id, value]])
|
|
117
|
+
};
|
|
118
|
+
onCleanup(() => clean(owner));
|
|
119
|
+
const prev = currentOwner;
|
|
120
|
+
currentOwner = owner;
|
|
121
|
+
try {
|
|
122
|
+
return fn();
|
|
123
|
+
} finally {
|
|
124
|
+
currentOwner = prev;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function useContext(ctx) {
|
|
128
|
+
for (let o = currentOwner; o; o = o.parent) {
|
|
129
|
+
if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
|
|
130
|
+
}
|
|
131
|
+
return ctx.defaultValue;
|
|
132
|
+
}
|
|
88
133
|
|
|
89
134
|
// src/dom/h.ts
|
|
90
135
|
function h(tag, props, ...children) {
|
|
@@ -118,12 +163,12 @@ function toNodes(value) {
|
|
|
118
163
|
return out;
|
|
119
164
|
}
|
|
120
165
|
function appendChildren(parent, children) {
|
|
121
|
-
const
|
|
122
|
-
while (
|
|
123
|
-
const child =
|
|
166
|
+
const pending2 = [...children].reverse();
|
|
167
|
+
while (pending2.length > 0) {
|
|
168
|
+
const child = pending2.pop();
|
|
124
169
|
if (Array.isArray(child)) {
|
|
125
170
|
for (let i = child.length - 1; i >= 0; i--) {
|
|
126
|
-
|
|
171
|
+
pending2.push(child[i]);
|
|
127
172
|
}
|
|
128
173
|
continue;
|
|
129
174
|
}
|
|
@@ -132,17 +177,29 @@ function appendChildren(parent, children) {
|
|
|
132
177
|
const end = document.createComment("");
|
|
133
178
|
parent.append(start, end);
|
|
134
179
|
createEffect(() => {
|
|
135
|
-
|
|
136
|
-
while (start.nextSibling && start.nextSibling !== end) {
|
|
137
|
-
start.nextSibling.remove();
|
|
138
|
-
}
|
|
139
|
-
end.before(...nodes);
|
|
180
|
+
reconcile(start, end, toNodes(child()));
|
|
140
181
|
});
|
|
141
182
|
} else {
|
|
142
183
|
parent.append(...toNodes(child));
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
}
|
|
187
|
+
function reconcile(start, end, next) {
|
|
188
|
+
const keep = new Set(next);
|
|
189
|
+
let cur = start.nextSibling;
|
|
190
|
+
while (cur && cur !== end) {
|
|
191
|
+
const after = cur.nextSibling;
|
|
192
|
+
if (!keep.has(cur)) cur.remove();
|
|
193
|
+
cur = after;
|
|
194
|
+
}
|
|
195
|
+
const parent = end.parentNode;
|
|
196
|
+
let ref = start.nextSibling;
|
|
197
|
+
for (const node of next) {
|
|
198
|
+
if (node === ref)
|
|
199
|
+
ref = ref.nextSibling;
|
|
200
|
+
else parent.insertBefore(node, ref);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
146
203
|
|
|
147
204
|
// src/dom/render.ts
|
|
148
205
|
function render(component, container) {
|
|
@@ -162,16 +219,54 @@ function Show(props) {
|
|
|
162
219
|
|
|
163
220
|
// src/components/for.ts
|
|
164
221
|
function For(props) {
|
|
165
|
-
|
|
222
|
+
let cache = /* @__PURE__ */ new Map();
|
|
223
|
+
onCleanup(() => {
|
|
224
|
+
cache.forEach((e) => e.dispose());
|
|
225
|
+
cache.clear();
|
|
226
|
+
});
|
|
227
|
+
return () => {
|
|
228
|
+
const items = props.each();
|
|
229
|
+
const next = /* @__PURE__ */ new Map();
|
|
230
|
+
const nodes = [];
|
|
231
|
+
items.forEach((item, i) => {
|
|
232
|
+
const k = props.key ? props.key(item, i) : item;
|
|
233
|
+
let entry = cache.get(k);
|
|
234
|
+
if (!entry) {
|
|
235
|
+
entry = createRoot((dispose) => ({
|
|
236
|
+
node: props.children(item, i),
|
|
237
|
+
dispose
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
next.set(k, entry);
|
|
241
|
+
nodes.push(entry.node);
|
|
242
|
+
});
|
|
243
|
+
cache.forEach((entry, k) => {
|
|
244
|
+
if (!next.has(k)) entry.dispose();
|
|
245
|
+
});
|
|
246
|
+
cache = next;
|
|
247
|
+
return nodes;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/reactivity/memo.ts
|
|
252
|
+
function createMemo(fn) {
|
|
253
|
+
const [value, setValue] = createSignal(void 0);
|
|
254
|
+
createEffect(() => setValue(() => fn()));
|
|
255
|
+
return value;
|
|
166
256
|
}
|
|
167
257
|
export {
|
|
168
258
|
For,
|
|
169
259
|
Show,
|
|
260
|
+
batch,
|
|
261
|
+
createContext,
|
|
170
262
|
createEffect,
|
|
263
|
+
createMemo,
|
|
171
264
|
createRoot,
|
|
172
265
|
createSignal,
|
|
173
266
|
h,
|
|
174
267
|
onCleanup,
|
|
175
268
|
onMount,
|
|
176
|
-
|
|
269
|
+
provide,
|
|
270
|
+
render,
|
|
271
|
+
useContext
|
|
177
272
|
};
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cajoa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A beginner-friendly, signals-based frontend framework.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.cjs",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": {
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"default": "./dist/index.js"
|
|
14
|
-
},
|
|
15
|
-
"require": {
|
|
16
|
-
"types": "./dist/index.d.cts",
|
|
17
|
-
"default": "./dist/index.cjs"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"sideEffects": false,
|
|
25
|
-
"scripts": {
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"dev": "vite --open /playground/",
|
|
28
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
29
|
-
"test": "vitest",
|
|
30
|
-
"prepublishOnly": "vitest run && npm run build"
|
|
31
|
-
},
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/cjmgrc/Cajoa.git"
|
|
35
|
-
},
|
|
36
|
-
"keywords": [
|
|
37
|
-
"frontend",
|
|
38
|
-
"framework",
|
|
39
|
-
"signals",
|
|
40
|
-
"reactive",
|
|
41
|
-
"ui",
|
|
42
|
-
"cajoa"
|
|
43
|
-
],
|
|
44
|
-
"author": "cjmgrc",
|
|
45
|
-
"license": "Apache-2.0",
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/cjmgrc/Cajoa/issues"
|
|
48
|
-
},
|
|
49
|
-
"homepage": "https://github.com/cjmgrc/Cajoa#readme",
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"jsdom": "^29.1.1",
|
|
52
|
-
"tsup": "^9.0.0",
|
|
53
|
-
"typescript": "^5.9.3",
|
|
54
|
-
"vite": "^8.3.4",
|
|
55
|
-
"vitest": "^5.0.5"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cajoa",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A beginner-friendly, signals-based frontend framework.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"dev": "vite --open /playground/",
|
|
28
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"prepublishOnly": "vitest run && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/cjmgrc/Cajoa.git"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"frontend",
|
|
38
|
+
"framework",
|
|
39
|
+
"signals",
|
|
40
|
+
"reactive",
|
|
41
|
+
"ui",
|
|
42
|
+
"cajoa"
|
|
43
|
+
],
|
|
44
|
+
"author": "cjmgrc",
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/cjmgrc/Cajoa/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/cjmgrc/Cajoa#readme",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"jsdom": "^29.1.1",
|
|
52
|
+
"tsup": "^9.0.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^8.3.4",
|
|
55
|
+
"vitest": "^5.0.5"
|
|
56
|
+
}
|
|
57
|
+
}
|