@3sln/dodo 0.0.5 → 0.0.7

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.
Files changed (3) hide show
  1. package/README.md +16 -111
  2. package/package.json +1 -1
  3. package/src/vdom.js +35 -37
package/README.md CHANGED
@@ -1,12 +1,26 @@
1
- # dodo
1
+ # Dodo
2
2
 
3
3
  > [!WARNING]
4
4
  > This is a work-in-progress project and is not yet ready for production use.
5
5
 
6
6
  A minimal, configurable virtual DOM library.
7
7
 
8
+ ## Documentation
9
+
10
+ For detailed documentation, live demos, and advanced usage examples, please check out the [Dodo Deck](https://dodo.3sln.com).
11
+
8
12
  ## Quick Start
9
13
 
14
+ Installation:
15
+ ```shell
16
+ npm install @3sln/dodo
17
+ # or
18
+ bun add @3sln/dodo
19
+ # or
20
+ yarn add @3sln/dodo
21
+ ```
22
+
23
+ Basic usage:
10
24
  ```javascript
11
25
  import { reconcile, h1, p, div } from '@3sln/dodo';
12
26
 
@@ -16,118 +30,9 @@ const container = document.getElementById('root');
16
30
  // The props object is optional.
17
31
  const myVdom = div({ id: 'app' },
18
32
  h1('Hello, dodo!'),
19
- p({ $classes: ['text-lg'] }, 'This is a paragraph.')
33
+ p('This is a paragraph.')
20
34
  );
21
35
 
22
36
  // Reconcile the virtual DOM with the real DOM.
23
37
  reconcile(container, [myVdom]);
24
- ```
25
-
26
- ## Core Concepts
27
-
28
- ## Core Concepts
29
-
30
- - **Factories for Customization:** Dodo is built around a factory pattern. While you can import and use a pre-configured default instance, you can also create custom instances for advanced use cases (like interoperating with ClojureScript). The library exports three factories:
31
- - `dodo(settings)`: A unified factory that returns a complete API with VDOM functions, HTML helpers, and the scheduler.
32
- - `vdom(settings)`: Creates just the core VDOM API (`h`, `alias`, `special`, `reconcile`, `settings`).
33
- - `html({ h })`: Creates the HTML helper functions (`div`, `p`, etc.) bound to a specific `h` function.
34
- - **HTML Helpers:** Simple functions like `div()`, `p()`, `span()` are exported for convenience.
35
- - **`$`-Prefixed Props:** Special props that `dodo` intercepts are prefixed with a `$` to avoid conflicts with standard properties (e.g., `$classes`, `$styling`, `$attrs`, `$dataset`).
36
- - **`.key()`:** Chain `.key('unique-id')` to any VNode in a list to enable efficient, keyed reconciliation.
37
- - **`.on()`:** Chain `.on({ event: handler })` to any VNode to attach event listeners or lifecycle hooks (`$attach`, `$detach`, `$update`).
38
- - **`.opaque()`**: Marks an element node as opaque, meaning `dodo` will manage its props but not its children.
39
-
40
- ## Keys for List Reconciliation
41
-
42
- When rendering lists of elements, always use the `.key()` method to ensure efficient updates and reordering.
43
-
44
- ```javascript
45
- import { reconcile, div } from '@3sln/dodo';
46
-
47
- const data = [{id: 1, text: 'A'}, {id: 2, text: 'B'}, {id: 3, text: 'C'}];
48
- const container = document.getElementById('list');
49
-
50
- // Chain .key() to the VNode returned by the helper function.
51
- reconcile(container, data.map(item => div(item.text).key(item.id)));
52
- ```
53
-
54
- ## Event Listeners and Lifecycle Hooks
55
-
56
- Use the `.on()` method to attach event listeners and lifecycle hooks.
57
-
58
- ```javascript
59
- import { reconcile, button } from '@3sln/dodo';
60
-
61
- const myButton = button('Hover over me').on({
62
- // Simple function handler
63
- mouseover: () => console.log('Mouse entered!'),
64
-
65
- // Object handler for more options
66
- mouseleave: {
67
- listener: () => console.log('Mouse left!'),
68
- capture: true
69
- },
70
-
71
- // Lifecycle hooks
72
- $attach: (domNode) => console.log('Button node was created.'),
73
- $detach: (domNode) => console.log('Button has been removed.')
74
- });
75
-
76
- const container = document.getElementById('event-container');
77
- reconcile(container, myButton);
78
- reconcile(container, null); // Triggers $detach
79
- ```
80
-
81
- ## Understanding `reconcile()`
82
-
83
- The `reconcile` function has two distinct modes of operation:
84
-
85
- - **`reconcile(target, vnode)` (Onto):** Modifies the `target` element itself to match the `vnode`. This requires the `target`'s tag name to be compatible with the `vnode`'s tag (unless the `vnode` is an `alias` or `special`).
86
-
87
- - **`reconcile(target, [vnodes...])` (Into):** Modifies the *children* of the `target` element to match the array of `vnodes`. The `target` element's own properties are not changed.
88
-
89
- - **`reconcile(target, null)` (Cleanup):** Detaches `dodo` from the `target` and its descendants, running all necessary cleanup logic.
90
-
91
- ## Advanced: Web Components with `special`
92
-
93
- The `special` component is a powerful tool for integrating with other libraries or, as shown here, for providing the rendering logic for a standard Web Component.
94
-
95
- ```javascript
96
- import { reconcile, special, p, h, alias } from '@3sln/dodo';
97
-
98
- // 1. Create a special component to manage a shadow root.
99
- const shadowComponent = special({
100
- attach: (domNode) => {
101
- // Create a shadow root on the host element once.
102
- domNode.attachShadow({ mode: 'open' });
103
- },
104
- update: (domNode, newContent) => {
105
- // Reconcile the provided content into the shadow root.
106
- reconcile(domNode.shadowRoot, newContent);
107
- }
108
- });
109
-
110
- // 2. Define a standard Web Component class.
111
- class MyComponent extends HTMLElement {
112
- connectedCallback() {
113
- // When the element is added to the DOM, render the special component ONTO it.
114
- // Pass some initial content into the special component's `update` hook.
115
- reconcile(this, shadowComponent(p('Hello from inside a web component!')));
116
- }
117
-
118
- disconnectedCallback() {
119
- // When the element is removed, clean up the dodo instance.
120
- reconcile(this, null);
121
- }
122
-
123
- // You can add attributeChangedCallback, properties, etc.
124
- // to re-reconcile with new content.
125
- }
126
-
127
- // 3. Register the custom element.
128
- customElements.define('my-component', MyComponent);
129
-
130
- // 4. Use your new custom element with dodo!
131
- const container = document.getElementById('my-component-container');
132
- reconcile(container, h('my-component'));
133
38
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3sln/dodo",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A minimal, configurable virtual DOM library.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/vdom.js CHANGED
@@ -13,7 +13,7 @@ class VNode {
13
13
  }
14
14
 
15
15
  key(k) {
16
- this.key = k;
16
+ this.k = k;
17
17
  return this;
18
18
  }
19
19
 
@@ -525,35 +525,33 @@ export default userSettings => {
525
525
 
526
526
  function cleanupTarget(target) {
527
527
  const state = target[NODE_STATE];
528
- if (state) {
529
- switch (state.vdom.type) {
530
- case ELEMENT_NODE:
531
- reconcileElementProps(target, {});
532
- cleanupTargetChildren(target);
533
- delete target[NODE_STATE];
534
- break;
535
- case OPAQUE_NODE:
536
- reconcileElementProps(target, {});
537
- delete target[NODE_STATE];
538
- break;
539
- case ALIAS_NODE:
540
- cleanupTargetChildren(target);
541
- delete target[NODE_STATE];
542
- break;
543
- case SPECIAL_NODE:
544
- delete target[NODE_STATE];
545
- try {
546
- state.vdom.tag.detach?.(target);
547
- } catch (err) {
548
- console.error(err);
549
- }
550
- break;
551
- }
552
- try {
553
- state.vdom.hooks?.$detach?.(target);
554
- } catch (err) {
555
- console.error(err);
528
+ if (!state) {
529
+ return;
530
+ }
531
+
532
+ const {vdom} = state;
533
+
534
+ if (vdom.hooks) {
535
+ reconcileListeners(target, EMPTY_MAP);
536
+ }
537
+
538
+ if (vdom.type === ELEMENT_NODE || vdom.type === OPAQUE_NODE) {
539
+ reconcileElementProps(target, EMPTY_MAP);
540
+ }
541
+
542
+ if (vdom.type === ELEMENT_NODE || vdom.type === ALIAS_NODE) {
543
+ cleanupTargetChildren(target);
544
+ }
545
+
546
+ delete target[NODE_STATE];
547
+
548
+ try {
549
+ if (vdom.type === SPECIAL_NODE) {
550
+ vdom.tag.detach?.(target);
556
551
  }
552
+ vdom.hooks?.$detach?.(target);
553
+ } catch (err) {
554
+ console.error(err);
557
555
  }
558
556
  }
559
557
 
@@ -581,17 +579,17 @@ export default userSettings => {
581
579
 
582
580
  let oldNodesPoolForTag = oldVNodeNodesPool.get(vdom.tag);
583
581
  if (!oldNodesPoolForTag) {
584
- oldNodesPoolForTag = {nodesForKey: new Map(), nodesWithoutKey: []};
582
+ oldNodesPoolForTag = {nodesForKey: newMap({}), nodesWithoutKey: []};
585
583
  oldVNodeNodesPool.set(vdom.tag, oldNodesPoolForTag);
586
584
  }
587
585
 
588
- if (vdom.key !== undefined) {
589
- let oldNodesPoolForKey = oldNodesPoolForTag.nodesForKey.get(vdom.key);
586
+ if (vdom.k !== undefined) {
587
+ let oldNodesPoolForKey = mapGet(oldNodesPoolForTag.nodesForKey, vdom.k);
590
588
  if (oldNodesPoolForKey === undefined) {
591
589
  oldNodesPoolForKey = [];
592
- oldNodesPoolForTag.nodesForKey.set(vdom.key, oldNodesPoolForKey);
593
590
  }
594
591
  oldNodesPoolForKey.push(oldChild);
592
+ oldNodesPoolForTag.nodesForKey = mapPut(oldNodesPoolForTag.nodesForKey, vdom.k, oldNodesPoolForKey);
595
593
  } else {
596
594
  oldNodesPoolForTag.nodesWithoutKey.push(oldChild);
597
595
  }
@@ -605,15 +603,15 @@ export default userSettings => {
605
603
  if (!oldNodesPoolForTag) {
606
604
  newDomNode = createNode(target, newVdom);
607
605
  } else {
608
- const key = newVdom.key;
606
+ const key = newVdom.k;
609
607
  if (key !== undefined) {
610
- const pool = oldNodesPoolForTag.nodesForKey.get(key);
608
+ const pool = mapGet(oldNodesPoolForTag.nodesForKey, key);
611
609
  if (pool && pool.length > 0) {
612
610
  newDomNode = pool.shift();
613
611
  const state = newDomNode[NODE_STATE];
614
612
  if (shouldUpdate(state.vdom.args, newVdom.args)) {
615
613
  if (state.vdom.hooks || newVdom.hooks) {
616
- reconcileListeners(target, newVdom.hooks);
614
+ reconcileListeners(newDomNode, newVdom.hooks);
617
615
  }
618
616
  state.newVdom = newVdom;
619
617
  }
@@ -627,7 +625,7 @@ export default userSettings => {
627
625
  const state = newDomNode[NODE_STATE];
628
626
  if (shouldUpdate(state.vdom.args, newVdom.args)) {
629
627
  if (state.vdom.hooks || newVdom.hooks) {
630
- reconcileListeners(target, newVdom.hooks);
628
+ reconcileListeners(newDomNode, newVdom.hooks);
631
629
  }
632
630
  state.newVdom = newVdom;
633
631
  }