@devchitchat/rdbljs 0.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.
Files changed (4) hide show
  1. package/README.md +550 -0
  2. package/index.js +1 -0
  3. package/package.json +54 -0
  4. package/src/rdbl.js +877 -0
package/README.md ADDED
@@ -0,0 +1,550 @@
1
+ # rdbl
2
+
3
+ **Reactive DOM Binding Library** — pronounced *"riddle"*
4
+
5
+ Signals-based reactivity wired directly to plain HTML attributes. No build step. No virtual DOM. No template expressions. Just readable markup and a single file.
6
+
7
+ ```html
8
+ <div text="user.name"></div>
9
+ <input model="query">
10
+ <ul each="results" key="id">
11
+ <template><li text="title"></li></template>
12
+ </ul>
13
+ ```
14
+
15
+ ```js
16
+ import { bind, signal, computed } from 'rdbl'
17
+
18
+ const state = {
19
+ user: { name: signal('Joey') },
20
+ query: signal(''),
21
+ results: signal([])
22
+ }
23
+
24
+ bind(document.querySelector('#app'), state)
25
+ ```
26
+
27
+ That's it.
28
+
29
+ ---
30
+
31
+ ## Why rdbl?
32
+
33
+ Most reactive libraries ask you to learn a new template language or move your logic into your markup. rdbl goes the other way: your HTML stays plain and readable, your JS stays in JS.
34
+
35
+ - **No `{{}}` expressions** — bindings are attribute paths, not code
36
+ - **No `x-` prefixes** — plain attributes: `text`, `show`, `model`, `each`
37
+ - **No build step** — single ES module, import and go
38
+ - **No virtual DOM** — direct DOM updates, batched via microtask
39
+ - **Signals all the way down** — `signal`, `computed`, `effect` are the entire reactive model
40
+
41
+ ---
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ bun add rdbl
47
+ ```
48
+
49
+ Or copy the single file directly — rdbl has no dependencies.
50
+
51
+ ```js
52
+ import { bind, signal, computed, effect, batch, Context, getItemContext, createScope } from 'rdbl'
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Core Concepts
58
+
59
+ ### Signals
60
+
61
+ A signal holds a value. Read it by calling it. Write it with `.set()`.
62
+
63
+ ```js
64
+ const count = signal(0)
65
+
66
+ count() // 0 — read
67
+ count.set(5) // write
68
+ count.peek() // 5 — read without tracking dependencies
69
+ ```
70
+
71
+ ### Computed
72
+
73
+ Derived state. Lazy-evaluated and cached until dependencies change.
74
+
75
+ ```js
76
+ const double = computed(() => count() * 2)
77
+ double() // 10
78
+ ```
79
+
80
+ ### Effect
81
+
82
+ Runs immediately and re-runs whenever its dependencies change.
83
+
84
+ ```js
85
+ effect(() => {
86
+ console.log('count is', count())
87
+ return () => console.log('cleanup') // optional
88
+ })
89
+ ```
90
+
91
+ ### Batch
92
+
93
+ Defer effect execution until all mutations are complete.
94
+
95
+ ```js
96
+ batch(() => {
97
+ a.set(1)
98
+ b.set(2)
99
+ // effects fire once, after batch
100
+ })
101
+ ```
102
+
103
+ ---
104
+
105
+ ## HTML Directives
106
+
107
+ Directives are plain HTML attributes. No special syntax — just a dot-separated path into your scope.
108
+
109
+ ### `text="path"`
110
+
111
+ Sets `textContent`.
112
+
113
+ ```html
114
+ <span text="user.name"></span>
115
+ ```
116
+
117
+ ### `html="path"`
118
+
119
+ Sets `innerHTML`.
120
+
121
+ ```html
122
+ <div html="article.body"></div>
123
+ ```
124
+
125
+ ### `show="path"`
126
+
127
+ Toggles visibility via the `hidden` attribute. Works with `<dialog>` — calls `showModal()` / `close()` automatically.
128
+
129
+ ```html
130
+ <div show="isVisible"></div>
131
+ <dialog show="modalOpen">...</dialog>
132
+ ```
133
+
134
+ ### `cls="path"`
135
+
136
+ Sets classes. Accepts a string (replaces `className`) or an object (toggles individual classes).
137
+
138
+ ```html
139
+ <div cls="statusClass"></div>
140
+ ```
141
+
142
+ ```js
143
+ // string form
144
+ statusClass: computed(() => isActive() ? 'active' : 'inactive')
145
+
146
+ // object form
147
+ statusClass: computed(() => ({ active: isActive(), disabled: isDisabled() }))
148
+ ```
149
+
150
+ ### `attr="name:path; name2:path2"`
151
+
152
+ Sets arbitrary attributes. Removes the attribute when value is `false` or `null`.
153
+
154
+ ```html
155
+ <button attr="aria-label:label; data-id:item.id"></button>
156
+ ```
157
+
158
+ ### `model="path"`
159
+
160
+ Two-way binding for form elements. Path must resolve to a signal.
161
+
162
+ ```html
163
+ <input model="query">
164
+ <input type="checkbox" model="isChecked">
165
+ <select model="selectedTab"></select>
166
+ <textarea model="notes"></textarea>
167
+ ```
168
+
169
+ ### `each="path" key="idPath"`
170
+
171
+ Keyed list rendering. Requires a `<template>` child. Efficiently diffs and reorders DOM nodes.
172
+
173
+ ```html
174
+ <ul each="todos" key="id">
175
+ <template>
176
+ <li>
177
+ <input type="checkbox" model="done">
178
+ <span text="text"></span>
179
+ <button onclick="removeTodo">x</button>
180
+ </li>
181
+ </template>
182
+ </ul>
183
+ ```
184
+
185
+ Inside list items, the scope includes all item properties directly, plus `$item` and `$index`.
186
+
187
+ ### `on<event>="path"`
188
+
189
+ Event binding. Path resolves to a function in your scope.
190
+
191
+ ```html
192
+ <button onclick="handleClick">Submit</button>
193
+ <input oninput="handleInput">
194
+ ```
195
+
196
+ The handler receives `(event, element, context)`. Return `false` to call both `preventDefault()` and `stopPropagation()`.
197
+
198
+ ```js
199
+ const state = {
200
+ handleClick(event, el, ctx) {
201
+ // do something
202
+ }
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## List Items and `getItemContext`
209
+
210
+ Inside an `each` list, use `getItemContext(element)` in your event handlers to access the item data for the row that was interacted with.
211
+
212
+ ```js
213
+ import { getItemContext } from 'rdbl'
214
+
215
+ const state = {
216
+ todos: signal([{ id: 1, text: 'write docs', done: false }]),
217
+
218
+ toggleTodo(event, el) {
219
+ const { item } = getItemContext(el)
220
+ state.todos.set(
221
+ state.todos().map(t => t.id === item.id ? { ...t, done: !t.done } : t)
222
+ )
223
+ }
224
+ }
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Context
230
+
231
+ DOM-scoped context, looked up by walking the element tree. Avoids prop drilling for shared services like routers or loggers.
232
+
233
+ ```js
234
+ import { Context } from 'rdbl'
235
+
236
+ Context.provide(document.querySelector('#app'), {
237
+ router: { go: path => history.pushState({}, '', path) },
238
+ log: console.log
239
+ })
240
+ ```
241
+
242
+ Context is available as the third argument in event handlers:
243
+
244
+ ```js
245
+ function handleNav(event, el, ctx) {
246
+ ctx.router.go('/dashboard')
247
+ }
248
+ ```
249
+
250
+ Read context directly anywhere you have an element:
251
+
252
+ ```js
253
+ const ctx = Context.read(someElement)
254
+ ```
255
+
256
+ ---
257
+
258
+ ## bind()
259
+
260
+ ```js
261
+ const app = bind(rootElement, scope, options)
262
+ ```
263
+
264
+ ### Options
265
+
266
+ | Option | Default | Description |
267
+ |---|---|---|
268
+ | `dev` | `true` | Log warnings for missing paths, bad values, missing keys |
269
+ | `autoBind` | `false` | Use `MutationObserver` to auto-bind dynamically added DOM |
270
+ | `ignoreSelector` | `[data-no-bind]` | CSS selector to opt subtrees out of binding |
271
+
272
+ ### `app.bindSubtree(element, scope)`
273
+
274
+ Bind a sub-element with its own scope. The sub-scope falls back to the parent scope for unresolved paths.
275
+
276
+ ```js
277
+ const app = bind(root, parentScope)
278
+ app.bindSubtree(cardElement, cardScope)
279
+ ```
280
+
281
+ ### `app.dispose()`
282
+
283
+ Tear down all effects and event listeners created by this binding.
284
+
285
+ ```js
286
+ app.dispose()
287
+ ```
288
+
289
+ ---
290
+
291
+ ## createScope()
292
+
293
+ Create a child scope that falls back to a parent for unresolved properties.
294
+
295
+ ```js
296
+ import { createScope } from 'rdbl'
297
+
298
+ const child = createScope(parentScope, {
299
+ title: signal('Local Title')
300
+ })
301
+ ```
302
+
303
+ ---
304
+
305
+ ## Full Example
306
+
307
+ ```html
308
+ <div id="app">
309
+ <h1 text="title"></h1>
310
+ <input model="newTodo" placeholder="Add a task...">
311
+ <button onclick="addTodo">Add</button>
312
+
313
+ <ul each="todos" key="id">
314
+ <template>
315
+ <li>
316
+ <input type="checkbox" model="done">
317
+ <span text="text" cls="itemClass"></span>
318
+ <button onclick="remove">Delete</button>
319
+ </li>
320
+ </template>
321
+ </ul>
322
+
323
+ <p text="summary"></p>
324
+ </div>
325
+ ```
326
+
327
+ ```js
328
+ import { bind, signal, computed, getItemContext } from 'rdbl'
329
+
330
+ const todos = signal([
331
+ { id: 1, text: 'Read the docs', done: false },
332
+ { id: 2, text: 'Build something', done: false }
333
+ ])
334
+
335
+ const newTodo = signal('')
336
+
337
+ const state = {
338
+ title: 'My Todos',
339
+ todos,
340
+ newTodo,
341
+ summary: computed(() => {
342
+ const all = todos()
343
+ const done = all.filter(t => t.done).length
344
+ return `${done} of ${all.length} done`
345
+ }),
346
+
347
+ addTodo() {
348
+ const text = newTodo().trim()
349
+ if (!text) return
350
+ todos.set([...todos(), { id: Date.now(), text, done: false }])
351
+ newTodo.set('')
352
+ },
353
+
354
+ remove(event, el) {
355
+ const { item } = getItemContext(el)
356
+ todos.set(todos().filter(t => t.id !== item.id))
357
+ }
358
+ }
359
+
360
+ bind(document.querySelector('#app'), state, { dev: true })
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Islands
366
+
367
+ Islands are independently bound components. Each element with an `[island]` attribute is its own binding root — rdbl stops traversal at nested island boundaries, so parent and child islands never interfere.
368
+
369
+ The `island` attribute value is a module path. Each island module exports a default factory function that receives the root element and `window`, reads shared state from `Context`, and returns its scope.
370
+
371
+ ### Island module
372
+
373
+ ```js
374
+ // /components/SyncStatus.js
375
+ import { computed, Context } from 'rdbl'
376
+
377
+ export default function syncStatus(root, window) {
378
+ const { pageState } = Context.read(document.body)
379
+
380
+ const statusText = computed(() =>
381
+ pageState.connected() ? 'Synced' : 'Connecting...'
382
+ )
383
+
384
+ const indicatorCls = computed(() =>
385
+ pageState.connected() ? 'dot synced' : 'dot pending'
386
+ )
387
+
388
+ return { statusText, indicatorCls }
389
+ }
390
+ ```
391
+
392
+ ### Island HTML
393
+
394
+ ```html
395
+ <section island="/components/SyncStatus.js">
396
+ <span cls="indicatorCls"></span>
397
+ <span text="statusText"></span>
398
+ </section>
399
+ ```
400
+
401
+ ### init() — binding all islands at startup
402
+
403
+ This pattern auto-discovers every `[island]` element, dynamically imports its module, and calls `bind()` with the returned scope.
404
+
405
+ ```js
406
+ import { bind, Context } from 'rdbl'
407
+
408
+ async function init(window) {
409
+ const roots = [...document.querySelectorAll('[island]')]
410
+ const instances = {}
411
+ let i = 0
412
+
413
+ for await (const root of roots) {
414
+ const key = root.getAttribute('island')
415
+ try {
416
+ const scopeFactory = (await import(key)).default
417
+ const scope = scopeFactory(root, window)
418
+ instances[`${key}:${i++}`] = bind(root, scope, { dev: true })
419
+ } catch (err) {
420
+ console.error(`Failed to load island "${key}":`, err)
421
+ }
422
+ }
423
+
424
+ return instances
425
+ }
426
+
427
+ // Provide shared state on the body so any island can read it
428
+ Context.provide(document.body, { pageState })
429
+
430
+ const app = await init(window)
431
+ ```
432
+
433
+ Each island gets its own binding instance and can be disposed independently. Shared state lives in `Context` — islands read it without being passed props.
434
+
435
+ ---
436
+
437
+ ## Server-Side Rendered Data
438
+
439
+ rdbl works naturally with server-rendered HTML. The key rule: **initialize your signals with server data before calling `bind()`**. On first run, effects write the same values the server already rendered — the DOM doesn't flicker.
440
+
441
+ ### Embedded JSON in a script tag
442
+
443
+ The server embeds initial state as JSON. The client reads it, hydrates signals, then binds.
444
+
445
+ ```html
446
+ <!-- Server renders this -->
447
+ <script type="application/json" id="page-data">
448
+ { "user": { "name": "Joey", "plan": "pro" }, "posts": [...] }
449
+ </script>
450
+
451
+ <div id="app" island="/components/Dashboard.js">
452
+ <h1 text="greeting"></h1>
453
+ <span text="user.plan"></span>
454
+ </div>
455
+ ```
456
+
457
+ ```js
458
+ // /components/Dashboard.js
459
+ import { signal, computed, Context } from 'rdbl'
460
+
461
+ export default function dashboard(root, window) {
462
+ const raw = JSON.parse(document.getElementById('page-data').textContent)
463
+
464
+ const user = signal(raw.user)
465
+ const posts = signal(raw.posts)
466
+
467
+ const greeting = computed(() => `Hello, ${user().name}`)
468
+
469
+ return { user, posts, greeting }
470
+ }
471
+ ```
472
+
473
+ ### Data attributes on the island root
474
+
475
+ For smaller per-component values, the server can write initial data directly onto the island element.
476
+
477
+ ```html
478
+ <section island="/components/UserBadge.js"
479
+ data-name="Joey"
480
+ data-plan="pro">
481
+ <span text="name"></span>
482
+ <span cls="badgeCls" text="plan"></span>
483
+ </section>
484
+ ```
485
+
486
+ ```js
487
+ // /components/UserBadge.js
488
+ import { signal, computed } from 'rdbl'
489
+
490
+ export default function userBadge(root, window) {
491
+ const name = signal(root.dataset.name)
492
+ const plan = signal(root.dataset.plan)
493
+ const badgeCls = computed(() => `badge badge-${plan()}`)
494
+
495
+ return { name, plan, badgeCls }
496
+ }
497
+ ```
498
+
499
+ ### Hydrating from storage
500
+
501
+ For client-persisted state (e.g. `localStorage`), read and apply the stored snapshot before binding. Effects fire with the restored values on first run, so the UI reflects the last known state immediately.
502
+
503
+ ```js
504
+ import { bind, signal, effect, Context } from 'rdbl'
505
+
506
+ const theme = signal('light')
507
+ const tasks = signal([])
508
+
509
+ // Hydrate signals from storage before bind()
510
+ const stored = localStorage.getItem('app-state')
511
+ if (stored) {
512
+ const snapshot = JSON.parse(stored)
513
+ theme.set(snapshot.theme ?? 'light')
514
+ tasks.set(snapshot.tasks ?? [])
515
+ }
516
+
517
+ // Persist on every change
518
+ effect(() => {
519
+ localStorage.setItem('app-state', JSON.stringify({
520
+ theme: theme(),
521
+ tasks: tasks()
522
+ }))
523
+ })
524
+
525
+ bind(document.querySelector('#app'), { theme, tasks })
526
+ ```
527
+
528
+ In all three patterns the principle is the same: signals are the source of truth, the server (or storage) provides the initial values, and `bind()` wires the already-correct state to the DOM.
529
+
530
+ ---
531
+
532
+ ## Design Principles
533
+
534
+ 1. **HTML is structure.** Directives describe *what* to bind, not *how*.
535
+ 2. **No expressions in markup.** Paths only — logic lives in JS.
536
+ 3. **Signals are the model.** One reactive primitive, no magic objects.
537
+ 4. **DOM-first.** No virtual DOM, no diffing overhead beyond list keys.
538
+ 5. **Single file.** Copy it, own it. No build pipeline required.
539
+
540
+ ---
541
+
542
+ ## Size
543
+
544
+ ~4KB gzipped. Zero dependencies.
545
+
546
+ ---
547
+
548
+ ## License
549
+
550
+ MIT
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { batch, signal, effect, computed, Context, getItemContext, createScope, bind } from './src/rdbl.js'
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@devchitchat/rdbljs",
3
+ "version": "0.0.0",
4
+ "description": "Signals-based reactive DOM binding. Plain HTML, no build step, no virtual DOM.",
5
+ "main": "index.js",
6
+ "module": "index.js",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "src/rdbl.js",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "bun test",
18
+ "docs:build:github": "cp src/rdbl.js docs/pages/public/assets/rdbl.js && bunx @devchitchat/index97@1.1.0 build docs/pages --out dist --path-prefix /rdbljs/",
19
+ "docs:build": "cp src/rdbl.js docs/pages/public/assets/rdbl.js && bunx @devchitchat/index97@1.1.0 build docs/pages --out dist",
20
+ "docs:serve": "cp src/rdbl.js docs/pages/public/assets/rdbl.js && bunx @devchitchat/index97@1.1.0 build docs/pages --out dist && bunx @devchitchat/index97@1.1.0 serve dist"
21
+ },
22
+ "keywords": [
23
+ "reactive",
24
+ "dom",
25
+ "signals",
26
+ "binding",
27
+ "no-build",
28
+ "vanilla"
29
+ ],
30
+ "author": "Joey Guerra",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/devchitchat/rdbljs.git"
35
+ },
36
+ "homepage": "https://github.com/devchitchat/rdbljs#readme",
37
+ "engines": {
38
+ "bun": ">=1.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "latest"
42
+ },
43
+ "peerDependencies": {
44
+ "typescript": "^5"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "typescript": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }