rask-ui 0.3.4 → 0.4.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 +251 -79
- package/dist/batch.d.ts +4 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +86 -0
- package/dist/component.d.ts +9 -1
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +15 -7
- package/dist/createComputed.d.ts +4 -0
- package/dist/createComputed.d.ts.map +1 -0
- package/dist/createComputed.js +34 -0
- package/dist/createEffect.d.ts +2 -0
- package/dist/createEffect.d.ts.map +1 -0
- package/dist/createEffect.js +19 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +4 -3
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +195 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +107 -0
- package/package.json +1 -1
- package/dist/createRef.d.ts +0 -5
- package/dist/createRef.d.ts.map +0 -1
- package/dist/createRef.js +0 -7
- package/dist/test-setup.d.ts +0 -16
- package/dist/test-setup.d.ts.map +0 -1
- package/dist/test-setup.js +0 -40
- package/dist/vdom/AbstractVNode.d.ts +0 -44
- package/dist/vdom/AbstractVNode.d.ts.map +0 -1
- package/dist/vdom/AbstractVNode.js +0 -256
- package/dist/vdom/ComponentVNode.d.ts +0 -48
- package/dist/vdom/ComponentVNode.d.ts.map +0 -1
- package/dist/vdom/ComponentVNode.js +0 -221
- package/dist/vdom/ElementVNode.d.ts +0 -27
- package/dist/vdom/ElementVNode.d.ts.map +0 -1
- package/dist/vdom/ElementVNode.js +0 -220
- package/dist/vdom/FragmentVNode.d.ts +0 -13
- package/dist/vdom/FragmentVNode.d.ts.map +0 -1
- package/dist/vdom/FragmentVNode.js +0 -49
- package/dist/vdom/RootVNode.d.ts +0 -25
- package/dist/vdom/RootVNode.d.ts.map +0 -1
- package/dist/vdom/RootVNode.js +0 -79
- package/dist/vdom/TextVNode.d.ts +0 -11
- package/dist/vdom/TextVNode.d.ts.map +0 -1
- package/dist/vdom/TextVNode.js +0 -35
- package/dist/vdom/dom-utils.d.ts +0 -14
- package/dist/vdom/dom-utils.d.ts.map +0 -1
- package/dist/vdom/dom-utils.js +0 -103
- package/dist/vdom/index.d.ts +0 -10
- package/dist/vdom/index.d.ts.map +0 -1
- package/dist/vdom/index.js +0 -26
- package/dist/vdom/types.d.ts +0 -20
- package/dist/vdom/types.d.ts.map +0 -1
- package/dist/vdom/types.js +0 -1
- package/dist/vdom/utils.d.ts +0 -6
- package/dist/vdom/utils.d.ts.map +0 -1
- package/dist/vdom/utils.js +0 -63
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<img src="logo.png" alt="Logo" width="200">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
A lightweight reactive component library that combines the simplicity of observable state management with the full power of a virtual DOM reconciler.
|
|
7
|
+
A lightweight reactive component library that combines the simplicity of observable state management with the full power of a virtual DOM reconciler. Ideal for single page applications, using web technology.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install rask-ui
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## The
|
|
13
|
+
## The Itch with Modern UI Frameworks
|
|
14
14
|
|
|
15
15
|
Modern UI frameworks present developers with a fundamental tradeoff between state management and UI expression:
|
|
16
16
|
|
|
@@ -42,9 +42,9 @@ function MyApp() {
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Solid offers a simpler mental model with fine-grained reactivity. Updates don't happen by calling the component function again,
|
|
45
|
+
Solid offers a seamingly simpler mental model with fine-grained reactivity. Updates don't happen by calling the component function again, which resolves the mental strain of expressing state management, however:
|
|
46
46
|
|
|
47
|
-
-
|
|
47
|
+
- The code you write is compiled to balance DX vs requirements of the runtime
|
|
48
48
|
- Special components for expressing dynamic UIs (`<Show>`, `<For>`, etc.)
|
|
49
49
|
- Different signatures for accessing reactive values: `count()` VS `state.count`
|
|
50
50
|
|
|
@@ -60,13 +60,13 @@ function MyApp() {
|
|
|
60
60
|
|
|
61
61
|
RASK gives you:
|
|
62
62
|
|
|
63
|
-
- **
|
|
64
|
-
- **Full reconciler power** - Express complex UIs naturally with
|
|
63
|
+
- **Simple state management** - No reconciler interference with your state management
|
|
64
|
+
- **Full reconciler power** - Express complex UIs naturally with the language
|
|
65
65
|
- **No special syntax** - Access state properties directly, no function calls
|
|
66
66
|
- **No compiler magic** - Plain JavaScript/TypeScript
|
|
67
|
-
- **Simple mental model** -
|
|
67
|
+
- **Simple mental model** - Just implement state and UI. No manual optimizations, special syntax or compiler magic
|
|
68
68
|
|
|
69
|
-
Built on [Inferno JS](https://github.com/infernojs/inferno).
|
|
69
|
+
:fire: Built on [Inferno JS](https://github.com/infernojs/inferno).
|
|
70
70
|
|
|
71
71
|
## Getting Started
|
|
72
72
|
|
|
@@ -182,9 +182,9 @@ function Parent() {
|
|
|
182
182
|
|
|
183
183
|
When `state.count` changes in Parent, only Child re-renders because it accesses `props.value`.
|
|
184
184
|
|
|
185
|
-
###
|
|
185
|
+
### One Rule To Accept
|
|
186
186
|
|
|
187
|
-
**RASK
|
|
187
|
+
**RASK has observable primitives**: Never destructure reactive objects (state, props, context values, async, query, mutation). Destructuring extracts plain values and breaks reactivity.
|
|
188
188
|
|
|
189
189
|
```tsx
|
|
190
190
|
// ❌ BAD - Destructuring breaks reactivity
|
|
@@ -234,6 +234,7 @@ Reactive objects are implemented using JavaScript Proxies. When you access a pro
|
|
|
234
234
|
- `createQuery()` - Never destructure query objects
|
|
235
235
|
- `createMutation()` - Never destructure mutation objects
|
|
236
236
|
- `createView()` - Never destructure view objects
|
|
237
|
+
- `createComputed()` - Never destructure computed objects
|
|
237
238
|
|
|
238
239
|
## API Reference
|
|
239
240
|
|
|
@@ -300,24 +301,26 @@ Creates a view that merges multiple objects (reactive or plain) into a single ob
|
|
|
300
301
|
```tsx
|
|
301
302
|
import { createView, createState } from "rask-ui";
|
|
302
303
|
|
|
303
|
-
function
|
|
304
|
+
function createCounter() {
|
|
304
305
|
const state = createState({ count: 0, name: "Counter" });
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
};
|
|
306
|
+
const increment = () => state.count++;
|
|
307
|
+
const decrement = () => state.count--;
|
|
308
|
+
const reset = () => (state.count = 0);
|
|
309
|
+
|
|
310
|
+
return createView(state, { increment, decrement, reset });
|
|
311
|
+
}
|
|
310
312
|
|
|
311
|
-
|
|
313
|
+
function Counter() {
|
|
314
|
+
const counter = createCounter();
|
|
312
315
|
|
|
313
316
|
return () => (
|
|
314
317
|
<div>
|
|
315
318
|
<h1>
|
|
316
|
-
{
|
|
319
|
+
{counter.name}: {counter.count}
|
|
317
320
|
</h1>
|
|
318
|
-
<button onClick={
|
|
319
|
-
<button onClick={
|
|
320
|
-
<button onClick={
|
|
321
|
+
<button onClick={counter.increment}>+</button>
|
|
322
|
+
<button onClick={counter.decrement}>-</button>
|
|
323
|
+
<button onClick={counter.reset}>Reset</button>
|
|
321
324
|
</div>
|
|
322
325
|
);
|
|
323
326
|
}
|
|
@@ -329,38 +332,6 @@ function Counter() {
|
|
|
329
332
|
|
|
330
333
|
**Returns:** A view object with getters for all properties, maintaining reactivity
|
|
331
334
|
|
|
332
|
-
**Use Cases:**
|
|
333
|
-
|
|
334
|
-
1. **Merge state with helper methods:**
|
|
335
|
-
|
|
336
|
-
```tsx
|
|
337
|
-
const state = createState({ count: 0 });
|
|
338
|
-
const helpers = { increment: () => state.count++ };
|
|
339
|
-
const view = createView(state, helpers);
|
|
340
|
-
// Access both: view.count, view.increment()
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
2. **Combine multiple reactive objects:**
|
|
344
|
-
|
|
345
|
-
```tsx
|
|
346
|
-
const user = createState({ name: "Alice" });
|
|
347
|
-
const settings = createState({ theme: "dark" });
|
|
348
|
-
const view = createView(user, settings);
|
|
349
|
-
// Access both: view.name, view.theme
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
3. **Override properties with computed values:**
|
|
353
|
-
```tsx
|
|
354
|
-
const state = createState({ firstName: "John", lastName: "Doe" });
|
|
355
|
-
const computed = {
|
|
356
|
-
get fullName() {
|
|
357
|
-
return `${state.firstName} ${state.lastName}`;
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
const view = createView(state, computed);
|
|
361
|
-
// view.fullName returns "John Doe" and updates when state changes
|
|
362
|
-
```
|
|
363
|
-
|
|
364
335
|
**Notes:**
|
|
365
336
|
|
|
366
337
|
- Reactivity is maintained through getters that reference the source objects
|
|
@@ -403,25 +374,170 @@ function Example() {
|
|
|
403
374
|
|
|
404
375
|
Pass the ref to an element's `ref` prop. The `current` property will be set to the DOM element when mounted and `null` when unmounted.
|
|
405
376
|
|
|
406
|
-
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### Reactivity Primitives
|
|
380
|
+
|
|
381
|
+
#### `createEffect(callback)`
|
|
382
|
+
|
|
383
|
+
Creates an effect that automatically tracks reactive dependencies and re-runs whenever they change. The effect runs immediately on creation.
|
|
407
384
|
|
|
408
385
|
```tsx
|
|
409
|
-
|
|
410
|
-
const svgRef = createRef<SVGSVGElement>();
|
|
386
|
+
import { createEffect, createState } from "rask-ui";
|
|
411
387
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
388
|
+
function Timer() {
|
|
389
|
+
const state = createState({ count: 0, log: [] });
|
|
390
|
+
|
|
391
|
+
// Effect runs immediately and whenever state.count changes
|
|
392
|
+
createEffect(() => {
|
|
393
|
+
console.log("Count changed:", state.count);
|
|
394
|
+
state.log.push(`Count: ${state.count}`);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return () => (
|
|
398
|
+
<div>
|
|
399
|
+
<p>Count: {state.count}</p>
|
|
400
|
+
<button onClick={() => state.count++}>Increment</button>
|
|
401
|
+
<ul>
|
|
402
|
+
{state.log.map((entry, i) => (
|
|
403
|
+
<li key={i}>{entry}</li>
|
|
404
|
+
))}
|
|
405
|
+
</ul>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Parameters:**
|
|
412
|
+
|
|
413
|
+
- `callback: () => void` - Function to run when dependencies change
|
|
414
|
+
|
|
415
|
+
**Features:**
|
|
416
|
+
|
|
417
|
+
- Runs immediately on creation
|
|
418
|
+
- Automatically tracks reactive dependencies accessed during execution
|
|
419
|
+
- Re-runs on microtask when dependencies change (prevents synchronous cascades)
|
|
420
|
+
- Automatically cleaned up when component unmounts
|
|
421
|
+
- Can be used for side effects like logging, syncing to localStorage, or updating derived state
|
|
422
|
+
|
|
423
|
+
**Notes:**
|
|
424
|
+
|
|
425
|
+
- Only call during component setup phase (not in render function)
|
|
426
|
+
- Effects are queued on microtask to avoid synchronous execution from prop changes
|
|
427
|
+
- Be careful with effects that modify state - can cause infinite loops if not careful
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
#### `createComputed<T>(computed)`
|
|
432
|
+
|
|
433
|
+
Creates an object with computed properties that automatically track dependencies and cache results until dependencies change.
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
import { createComputed, createState } from "rask-ui";
|
|
437
|
+
|
|
438
|
+
function ShoppingCart() {
|
|
439
|
+
const state = createState({
|
|
440
|
+
items: [
|
|
441
|
+
{ id: 1, name: "Apple", price: 1.5, quantity: 3 },
|
|
442
|
+
{ id: 2, name: "Banana", price: 0.8, quantity: 5 },
|
|
443
|
+
],
|
|
444
|
+
taxRate: 0.2,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const computed = createComputed({
|
|
448
|
+
subtotal: () =>
|
|
449
|
+
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
450
|
+
tax: () => computed.subtotal * state.taxRate,
|
|
451
|
+
total: () => computed.subtotal + computed.tax,
|
|
452
|
+
itemCount: () =>
|
|
453
|
+
state.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return () => (
|
|
457
|
+
<div>
|
|
458
|
+
<h2>Cart ({computed.itemCount} items)</h2>
|
|
459
|
+
<ul>
|
|
460
|
+
{state.items.map((item) => (
|
|
461
|
+
<li key={item.id}>
|
|
462
|
+
{item.name}: ${item.price} x {item.quantity}
|
|
463
|
+
<button onClick={() => item.quantity++}>+</button>
|
|
464
|
+
<button onClick={() => item.quantity--}>-</button>
|
|
465
|
+
</li>
|
|
466
|
+
))}
|
|
467
|
+
</ul>
|
|
468
|
+
<div>
|
|
469
|
+
<p>Subtotal: ${computed.subtotal.toFixed(2)}</p>
|
|
470
|
+
<p>Tax ({state.taxRate * 100}%): ${computed.tax.toFixed(2)}</p>
|
|
471
|
+
<p>
|
|
472
|
+
<strong>Total: ${computed.total.toFixed(2)}</strong>
|
|
473
|
+
</p>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Parameters:**
|
|
481
|
+
|
|
482
|
+
- `computed: T` - Object where each property is a function returning a computed value
|
|
483
|
+
|
|
484
|
+
**Returns:** Reactive object with cached computed properties
|
|
485
|
+
|
|
486
|
+
**Features:**
|
|
487
|
+
|
|
488
|
+
- **Lazy evaluation** - Computed values are only calculated when accessed
|
|
489
|
+
- **Automatic caching** - Results are cached until dependencies change
|
|
490
|
+
- **Dependency tracking** - Automatically tracks what state each computed depends on
|
|
491
|
+
- **Composable** - Computed properties can depend on other computed properties
|
|
492
|
+
- **Efficient** - Only recomputes when dirty (dependencies changed)
|
|
493
|
+
- **Automatic cleanup** - Cleaned up when component unmounts
|
|
494
|
+
|
|
495
|
+
**Notes:**
|
|
496
|
+
|
|
497
|
+
- Access computed properties directly (e.g., `computed.total`), don't call as functions
|
|
498
|
+
- Computed properties are getters, not functions
|
|
499
|
+
- **Do not destructure** - Breaks reactivity (see warning section above)
|
|
500
|
+
- Only call during component setup phase
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
### Automatic Batching
|
|
505
|
+
|
|
506
|
+
RASK automatically batches state updates to minimize re-renders. This happens transparently without any special syntax.
|
|
507
|
+
|
|
508
|
+
**How it works:**
|
|
509
|
+
|
|
510
|
+
- **User interactions** (clicks, inputs, keyboard, etc.) - State changes are batched and flushed synchronously at the end of the event
|
|
511
|
+
- **Other updates** (setTimeout, fetch callbacks, etc.) - State changes are batched and flushed on the next microtask
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
function BatchingExample() {
|
|
515
|
+
const state = createState({ count: 0, clicks: 0 });
|
|
516
|
+
|
|
517
|
+
const handleClick = () => {
|
|
518
|
+
// All three updates are batched into a single render
|
|
519
|
+
state.count++;
|
|
520
|
+
state.clicks++;
|
|
521
|
+
state.count++;
|
|
522
|
+
// UI updates once with count=2, clicks=1
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const handleAsync = () => {
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
// These updates are also batched (async batch)
|
|
528
|
+
state.count++;
|
|
529
|
+
state.clicks++;
|
|
530
|
+
// UI updates once on next microtask
|
|
531
|
+
}, 100);
|
|
417
532
|
};
|
|
418
533
|
|
|
419
534
|
return () => (
|
|
420
535
|
<div>
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
</
|
|
424
|
-
<button onClick={
|
|
536
|
+
<p>
|
|
537
|
+
Count: {state.count}, Clicks: {state.clicks}
|
|
538
|
+
</p>
|
|
539
|
+
<button onClick={handleClick}>Sync Update</button>
|
|
540
|
+
<button onClick={handleAsync}>Async Update</button>
|
|
425
541
|
</div>
|
|
426
542
|
);
|
|
427
543
|
}
|
|
@@ -543,13 +659,16 @@ import { createAsync } from "rask-ui";
|
|
|
543
659
|
function UserProfile() {
|
|
544
660
|
const user = createAsync(fetch("/api/user").then((r) => r.json()));
|
|
545
661
|
|
|
546
|
-
return () =>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
662
|
+
return () => }
|
|
663
|
+
if (user.isPending) {
|
|
664
|
+
return <p>Loading...</p>
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (user.error) {
|
|
668
|
+
return <p>Error: {user.error}</p>
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return <p>Hello, {user.value.name}!</p>
|
|
553
672
|
}
|
|
554
673
|
```
|
|
555
674
|
|
|
@@ -580,16 +699,25 @@ import { createQuery } from "rask-ui";
|
|
|
580
699
|
|
|
581
700
|
function Posts() {
|
|
582
701
|
const posts = createQuery(() => fetch("/api/posts").then((r) => r.json()));
|
|
702
|
+
const renderPosts = () => {
|
|
703
|
+
if (posts.isPending) {
|
|
704
|
+
return <p>Loading...</p>;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (posts.error) {
|
|
708
|
+
return <p>Error: {posts.error}</p>;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return posts.data.map((post) => (
|
|
712
|
+
<article key={post.id}>{post.title}</article>
|
|
713
|
+
));
|
|
714
|
+
};
|
|
583
715
|
|
|
584
716
|
return () => (
|
|
585
717
|
<div>
|
|
586
718
|
<button onClick={() => posts.fetch()}>Refresh</button>
|
|
587
719
|
<button onClick={() => posts.fetch(true)}>Force Refresh</button>
|
|
588
|
-
|
|
589
|
-
{posts.isPending && <p>Loading...</p>}
|
|
590
|
-
{posts.error && <p>Error: {posts.error}</p>}
|
|
591
|
-
{posts.data &&
|
|
592
|
-
posts.data.map((post) => <article key={post.id}>{post.title}</article>)}
|
|
720
|
+
{renderPosts()}
|
|
593
721
|
</div>
|
|
594
722
|
);
|
|
595
723
|
}
|
|
@@ -630,7 +758,7 @@ function CreatePost() {
|
|
|
630
758
|
fetch("/api/posts", {
|
|
631
759
|
method: "POST",
|
|
632
760
|
body: JSON.stringify(data),
|
|
633
|
-
})
|
|
761
|
+
}))
|
|
634
762
|
);
|
|
635
763
|
|
|
636
764
|
const handleSubmit = () => {
|
|
@@ -768,7 +896,9 @@ Keys prevent component recreation when list order changes.
|
|
|
768
896
|
|
|
769
897
|
### Computed Values
|
|
770
898
|
|
|
771
|
-
|
|
899
|
+
You can create computed values in two ways:
|
|
900
|
+
|
|
901
|
+
**1. Simple computed functions** - For basic derived values:
|
|
772
902
|
|
|
773
903
|
```tsx
|
|
774
904
|
function ShoppingCart() {
|
|
@@ -799,6 +929,48 @@ function ShoppingCart() {
|
|
|
799
929
|
|
|
800
930
|
Computed functions automatically track dependencies when called during render.
|
|
801
931
|
|
|
932
|
+
**2. Using `createComputed`** - For cached, efficient computed values with automatic dependency tracking:
|
|
933
|
+
|
|
934
|
+
```tsx
|
|
935
|
+
function ShoppingCart() {
|
|
936
|
+
const state = createState({
|
|
937
|
+
items: [
|
|
938
|
+
{ id: 1, price: 10, quantity: 2 },
|
|
939
|
+
{ id: 2, price: 20, quantity: 1 },
|
|
940
|
+
],
|
|
941
|
+
taxRate: 0.1,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const computed = createComputed({
|
|
945
|
+
subtotal: () =>
|
|
946
|
+
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
947
|
+
tax: () => computed.subtotal * state.taxRate,
|
|
948
|
+
total: () => computed.subtotal + computed.tax,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
return () => (
|
|
952
|
+
<div>
|
|
953
|
+
<ul>
|
|
954
|
+
{state.items.map((item) => (
|
|
955
|
+
<li key={item.id}>
|
|
956
|
+
${item.price} x {item.quantity}
|
|
957
|
+
</li>
|
|
958
|
+
))}
|
|
959
|
+
</ul>
|
|
960
|
+
<p>Subtotal: ${computed.subtotal}</p>
|
|
961
|
+
<p>Tax: ${computed.tax}</p>
|
|
962
|
+
<p>Total: ${computed.total}</p>
|
|
963
|
+
</div>
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Benefits of `createComputed`:
|
|
969
|
+
- **Cached** - Only recalculates when dependencies change
|
|
970
|
+
- **Lazy** - Only calculates when accessed
|
|
971
|
+
- **Composable** - Computed properties can depend on other computed properties
|
|
972
|
+
- **Efficient** - Better performance for expensive calculations
|
|
973
|
+
|
|
802
974
|
### Composition
|
|
803
975
|
|
|
804
976
|
Compose complex logic by combining state and methods using `createView`:
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAmDA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,QAWnC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAOvC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,WAAsB,QA0BlE"}
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const INTERACTIVE_EVENTS = [
|
|
2
|
+
// DISCRETE
|
|
3
|
+
"beforeinput",
|
|
4
|
+
"input",
|
|
5
|
+
"change",
|
|
6
|
+
"compositionend",
|
|
7
|
+
"keydown",
|
|
8
|
+
"keyup",
|
|
9
|
+
"click",
|
|
10
|
+
"contextmenu",
|
|
11
|
+
"submit",
|
|
12
|
+
"reset",
|
|
13
|
+
// GESTURE START
|
|
14
|
+
"pointerdown",
|
|
15
|
+
"mousedown",
|
|
16
|
+
"touchstart",
|
|
17
|
+
// GESTURE END
|
|
18
|
+
"pointerup",
|
|
19
|
+
"mouseup",
|
|
20
|
+
"touchend",
|
|
21
|
+
"touchcancel",
|
|
22
|
+
];
|
|
23
|
+
let inInteractive = 0;
|
|
24
|
+
let hasAsyncQueue = false;
|
|
25
|
+
let hasSyncQueue = false;
|
|
26
|
+
const flushQueue = new Set();
|
|
27
|
+
const syncFlushQueue = new Set();
|
|
28
|
+
function queueAsync() {
|
|
29
|
+
if (hasAsyncQueue) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
hasAsyncQueue = true;
|
|
33
|
+
queueMicrotask(() => {
|
|
34
|
+
hasAsyncQueue = false;
|
|
35
|
+
if (!flushQueue.size) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const queued = Array.from(flushQueue);
|
|
39
|
+
flushQueue.clear();
|
|
40
|
+
queued.forEach((cb) => cb());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function queue(cb) {
|
|
44
|
+
if (hasSyncQueue) {
|
|
45
|
+
syncFlushQueue.add(cb);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
flushQueue.add(cb);
|
|
49
|
+
if (!inInteractive) {
|
|
50
|
+
queueAsync();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function syncBatch(cb) {
|
|
54
|
+
hasSyncQueue = true;
|
|
55
|
+
cb();
|
|
56
|
+
hasSyncQueue = false;
|
|
57
|
+
const queued = Array.from(syncFlushQueue);
|
|
58
|
+
syncFlushQueue.clear();
|
|
59
|
+
queued.forEach((cb) => cb());
|
|
60
|
+
}
|
|
61
|
+
export function installEventBatching(target = document) {
|
|
62
|
+
const captureOptions = { capture: true, passive: true };
|
|
63
|
+
const bubbleOptions = { passive: true };
|
|
64
|
+
const onCapture = () => {
|
|
65
|
+
inInteractive++;
|
|
66
|
+
// Backup in case of stop propagation
|
|
67
|
+
queueAsync();
|
|
68
|
+
};
|
|
69
|
+
const onBubble = () => {
|
|
70
|
+
if (--inInteractive === 0 && flushQueue.size) {
|
|
71
|
+
const queued = Array.from(flushQueue);
|
|
72
|
+
flushQueue.clear();
|
|
73
|
+
queued.forEach((cb) => cb());
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// 1) open scope before handlers
|
|
77
|
+
INTERACTIVE_EVENTS.forEach((type) => {
|
|
78
|
+
target.addEventListener(type, onCapture, captureOptions);
|
|
79
|
+
});
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
// 2) close + flush after handlers (bubble on window/document)
|
|
82
|
+
INTERACTIVE_EVENTS.forEach((type) => {
|
|
83
|
+
target.addEventListener(type, onBubble, bubbleOptions);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/component.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ declare class RaskComponent<P extends Props<any>> extends Component<P & {
|
|
|
10
10
|
private reactiveProps?;
|
|
11
11
|
private observer;
|
|
12
12
|
private isRendering;
|
|
13
|
+
effects: Array<{
|
|
14
|
+
isDirty: boolean;
|
|
15
|
+
run: () => void;
|
|
16
|
+
}>;
|
|
13
17
|
contexts: Map<any, any>;
|
|
14
18
|
getChildContext(): any;
|
|
15
19
|
onMounts: Array<() => void>;
|
|
@@ -17,7 +21,11 @@ declare class RaskComponent<P extends Props<any>> extends Component<P & {
|
|
|
17
21
|
private createReactiveProps;
|
|
18
22
|
componentDidMount(): void;
|
|
19
23
|
componentWillUnmount(): void;
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
componentWillUpdate(nextProps: any): void;
|
|
28
|
+
componentWillReceiveProps(): void;
|
|
21
29
|
shouldComponentUpdate(nextProps: Props<any>): boolean;
|
|
22
30
|
render(): any;
|
|
23
31
|
}
|
package/dist/component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAOjB,wBAAgB,mBAAmB,uBAMlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAMrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAMvC;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAClD,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,cAAM,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CACzD,CAAC,GAAG;IAAE,WAAW,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAA;CAAE,CAC9C;IACC,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IAgC3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAerD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
|
package/dist/component.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createComponentVNode, Component, } from "inferno";
|
|
2
2
|
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
3
|
+
import { syncBatch } from "./batch";
|
|
3
4
|
let currentComponent;
|
|
4
5
|
export function getCurrentComponent() {
|
|
5
6
|
if (!currentComponent) {
|
|
@@ -26,6 +27,7 @@ class RaskComponent extends Component {
|
|
|
26
27
|
this.forceUpdate();
|
|
27
28
|
});
|
|
28
29
|
isRendering = false;
|
|
30
|
+
effects = [];
|
|
29
31
|
contexts = new Map();
|
|
30
32
|
getChildContext() {
|
|
31
33
|
const parentGetContext = this.context.getContext;
|
|
@@ -72,15 +74,21 @@ class RaskComponent extends Component {
|
|
|
72
74
|
componentWillUnmount() {
|
|
73
75
|
this.onCleanups.forEach((cb) => cb());
|
|
74
76
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
/**
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
componentWillUpdate(nextProps) {
|
|
81
|
+
syncBatch(() => {
|
|
82
|
+
for (const prop in nextProps) {
|
|
83
|
+
if (prop === "__component" || prop === "children") {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// @ts-ignore
|
|
87
|
+
this.reactiveProps[prop] = nextProps[prop];
|
|
79
88
|
}
|
|
80
|
-
|
|
81
|
-
this.reactiveProps[prop] = this.props[prop];
|
|
82
|
-
}
|
|
89
|
+
});
|
|
83
90
|
}
|
|
91
|
+
componentWillReceiveProps() { }
|
|
84
92
|
shouldComponentUpdate(nextProps) {
|
|
85
93
|
// Shallow comparison of props, excluding internal props
|
|
86
94
|
for (const prop in nextProps) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createComputed.d.ts","sourceRoot":"","sources":["../src/createComputed.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAChE,QAAQ,EAAE,CAAC,GACV;KACA,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CAsCA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getCurrentComponent, onCleanup } from "./component";
|
|
2
|
+
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
3
|
+
export function createComputed(computed) {
|
|
4
|
+
const currentComponent = getCurrentComponent();
|
|
5
|
+
const proxy = {};
|
|
6
|
+
for (const prop in computed) {
|
|
7
|
+
let isDirty = true;
|
|
8
|
+
let value;
|
|
9
|
+
const signal = new Signal();
|
|
10
|
+
const observer = new Observer(() => {
|
|
11
|
+
isDirty = true;
|
|
12
|
+
signal.notify();
|
|
13
|
+
});
|
|
14
|
+
if (currentComponent) {
|
|
15
|
+
onCleanup(() => observer.dispose());
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(proxy, prop, {
|
|
18
|
+
get() {
|
|
19
|
+
const observer = getCurrentObserver();
|
|
20
|
+
if (observer) {
|
|
21
|
+
observer.subscribeSignal(signal);
|
|
22
|
+
}
|
|
23
|
+
if (isDirty) {
|
|
24
|
+
const stopObserving = observer.observe();
|
|
25
|
+
value = computed[prop]();
|
|
26
|
+
stopObserving();
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return proxy;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAmB1C"}
|