dom-primitive-bindings 0.0.0-stage → 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/LICENSE +22 -0
- package/README.md +125 -2
- package/docs/dist/min.js +63 -0
- package/docs/dist/v1/min.js +63 -0
- package/docs/index.html +185 -0
- package/package.json +34 -4
- package/src/index.js +3 -0
- package/src/v1/dispatch/executeBinding.js +21 -0
- package/src/v1/dispatch/getClientData.js +8 -0
- package/src/v1/dispatch/getStartInput.js +10 -0
- package/src/v1/dispatch/index.js +15 -0
- package/src/v1/guards/bindings/index.js +11 -0
- package/src/v1/guards/bindings/validateArray.js +10 -0
- package/src/v1/guards/container/index.js +13 -0
- package/src/v1/guards/container/resolveElement.js +14 -0
- package/src/v1/guards/index.js +15 -0
- package/src/v1/index.js +22 -0
- package/src/v1/listeners/attachListener.js +10 -0
- package/src/v1/listeners/index.js +12 -0
- package/src/v1/prepare/groupBindings.js +14 -0
- package/src/v1/prepare/index.js +8 -0
- package/src/v1/register/index.js +17 -0
- package/src/v1/register/meta.js +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KeshavSoft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DOM Primitive Bindings
|
|
2
|
+
|
|
3
|
+
> Modern, lightweight, delegated event engine for browser DOMs. Attach exactly one listener per event type to any container; let bubbling handle matching, runners, and extraction with zero re-binding overhead.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🎯 The Core Concept
|
|
12
|
+
|
|
13
|
+
Traditional client-side code attaches listeners to every button, link, and input. When tables are refreshed, cards are added, or modals render dynamic markup, those listeners break or leak memory unless manually re-attached.
|
|
14
|
+
|
|
15
|
+
**DOM Primitive Bindings** inverts the architecture:
|
|
16
|
+
- Attach **one listener** per event type to the parent container (`table`, `form`, `card-grid`).
|
|
17
|
+
- As events bubble up, `closest(selector)` identifies target elements.
|
|
18
|
+
- The associated `runner` executes and forwards data cleanly to `onExtract`.
|
|
19
|
+
- Dynamically created DOM elements are supported immediately with **zero re-binding**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🏗️ Staged Architecture (`src/v1/`)
|
|
24
|
+
|
|
25
|
+
The repository strictly enforces a staged pipeline where every directory contains an `index.js` and all files export a single function as `default`:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
src/
|
|
29
|
+
└── v1/
|
|
30
|
+
├── guards/ # Validates container and bindings array
|
|
31
|
+
│ ├── container/ # Validates and resolves 1st input (container / ID)
|
|
32
|
+
│ ├── bindings/ # Validates 2nd input (bindings array)
|
|
33
|
+
│ └── index.js
|
|
34
|
+
├── prepare/ # Groups bindings by event type into Map
|
|
35
|
+
│ ├── groupBindings.js
|
|
36
|
+
│ └── index.js
|
|
37
|
+
├── listeners/ # Attaches single listener per event type to container
|
|
38
|
+
│ ├── attachListener.js
|
|
39
|
+
│ └── index.js
|
|
40
|
+
├── dispatch/ # Handles event bubbling, runner execution & extraction
|
|
41
|
+
│ ├── getStartInput.js
|
|
42
|
+
│ ├── getClientData.js
|
|
43
|
+
│ ├── executeBinding.js
|
|
44
|
+
│ └── index.js
|
|
45
|
+
├── register/ # Exposes module onto window.ks.domPrimitiveBindings
|
|
46
|
+
│ ├── meta.js
|
|
47
|
+
│ └── index.js
|
|
48
|
+
└── index.js # Orchestrator exporting bindEvents
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Key Design Disciplines
|
|
52
|
+
1. **Zero Comments:** The folder hierarchy and descriptive file names convey the full narrative without inline code comments.
|
|
53
|
+
2. **Folder Discipline:** Every folder contains an `index.js` public gateway.
|
|
54
|
+
3. **Single Function Default Export:** No default export objects; every file exports a single function.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🚀 Interactive Browser Samples (`samples/v1/`)
|
|
59
|
+
|
|
60
|
+
Start the Vite development server to view and interact with real browser DOM events:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
npm run dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Open in Chrome or Edge:
|
|
68
|
+
- **`samples/v1/tableWithSearch/`** — Delegated table row clicks and live Enter-key search input.
|
|
69
|
+
- **`samples/v1/cardGrid/`** — Dynamic card insertion into DOM with instant delegated action buttons.
|
|
70
|
+
- **`samples/v1/formEvents/`** — Live form field inputs, dropdown selection changes, and submit/clear buttons.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 📦 Usage
|
|
75
|
+
|
|
76
|
+
### Browser (ES Module / Script)
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<script type="module" src="./docs/dist/v1/min.js"></script>
|
|
80
|
+
<script type="module">
|
|
81
|
+
const bindings = [
|
|
82
|
+
{
|
|
83
|
+
event: "click",
|
|
84
|
+
selector: ".btn-cart",
|
|
85
|
+
runner: (target) => ({ productId: target.dataset.id }),
|
|
86
|
+
onExtract: (data) => console.log("Added to cart:", data)
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
window.ks.domPrimitiveBindings.bindEvents("products-container", bindings);
|
|
91
|
+
</script>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Module Import
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
import { bindEvents } from "dom-primitive-bindings";
|
|
98
|
+
|
|
99
|
+
bindEvents(containerElement, [
|
|
100
|
+
{
|
|
101
|
+
event: "click",
|
|
102
|
+
selector: ".action-btn",
|
|
103
|
+
runner: (btn) => btn.dataset.action,
|
|
104
|
+
onExtract: (action) => console.log("Action:", action)
|
|
105
|
+
}
|
|
106
|
+
]);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🧪 Testing & Build
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Run unit tests
|
|
115
|
+
npm test
|
|
116
|
+
|
|
117
|
+
# Build production bundle to docs/dist/v1/min.js
|
|
118
|
+
npm run build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📄 License
|
|
124
|
+
|
|
125
|
+
MIT © KeshavSoft
|
|
2
126
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/docs/dist/min.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const u = {
|
|
2
|
+
name: "dom-primitive-bindings",
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
}, c = (t, n = u) => {
|
|
5
|
+
if (!(!t || typeof globalThis > "u"))
|
|
6
|
+
return globalThis.ks ?? (globalThis.ks = {}), globalThis.ks.domPrimitiveBindings = {
|
|
7
|
+
meta: n,
|
|
8
|
+
bindEvents: t
|
|
9
|
+
}, globalThis.ks.domPrimitiveBindings;
|
|
10
|
+
}, l = (t) => typeof t == "string" ? document.getElementById(t) || document.querySelector(t) : t && typeof t == "object" && "addEventListener" in t ? t : null, a = (t) => {
|
|
11
|
+
if (!t) return null;
|
|
12
|
+
const n = l(t);
|
|
13
|
+
return n || null;
|
|
14
|
+
}, f = (t) => Array.isArray(t) ? t.filter((n) => n && typeof n == "object" && typeof n.event == "string" && typeof n.selector == "string") : [], g = (t) => {
|
|
15
|
+
const n = f(t);
|
|
16
|
+
return n.length === 0 ? null : n;
|
|
17
|
+
}, p = (t, n) => {
|
|
18
|
+
const e = a(t);
|
|
19
|
+
if (!e) return null;
|
|
20
|
+
const r = g(n);
|
|
21
|
+
return r ? { container: e, bindings: r } : null;
|
|
22
|
+
}, d = (t) => {
|
|
23
|
+
const n = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const e of t) {
|
|
25
|
+
const r = n.get(e.event) ?? [];
|
|
26
|
+
r.push(e), n.set(e.event, r);
|
|
27
|
+
}
|
|
28
|
+
return n;
|
|
29
|
+
}, y = (t) => d(t), m = (t, n, e) => {
|
|
30
|
+
var r;
|
|
31
|
+
return t.root === "event" || (r = n == null ? void 0 : n.type) != null && r.startsWith("key") ? n : e;
|
|
32
|
+
}, h = (t, n, e) => ({
|
|
33
|
+
...t.clientData,
|
|
34
|
+
event: n,
|
|
35
|
+
targetElement: e
|
|
36
|
+
}), v = ({ binding: t, event: n, matchElement: e }) => {
|
|
37
|
+
const r = m(t, n, e), i = h(t, n, e), o = t.runner, s = typeof o == "function" ? o(r, t.instructions, i) : r;
|
|
38
|
+
return s != null && typeof t.onExtract == "function" && t.onExtract(s, n, e), s;
|
|
39
|
+
}, B = ({ bindings: t, event: n, container: e }) => {
|
|
40
|
+
var r, i;
|
|
41
|
+
for (const o of t) {
|
|
42
|
+
const s = (i = (r = n.target) == null ? void 0 : r.closest) == null ? void 0 : i.call(r, o.selector);
|
|
43
|
+
!s || !e.contains(s) || v({ binding: o, event: n, matchElement: s });
|
|
44
|
+
}
|
|
45
|
+
}, E = (t, n, e) => {
|
|
46
|
+
t.addEventListener(n, (r) => {
|
|
47
|
+
B({ bindings: e, event: r, container: t });
|
|
48
|
+
});
|
|
49
|
+
}, k = (t, n) => {
|
|
50
|
+
for (const [e, r] of n.entries())
|
|
51
|
+
E(t, e, r);
|
|
52
|
+
return t;
|
|
53
|
+
}, T = (t, n = []) => {
|
|
54
|
+
const e = p(t, n);
|
|
55
|
+
if (!e) return null;
|
|
56
|
+
const { container: r, bindings: i } = e, o = y(i);
|
|
57
|
+
return k(r, o), r;
|
|
58
|
+
};
|
|
59
|
+
c(T);
|
|
60
|
+
export {
|
|
61
|
+
T as bindEvents,
|
|
62
|
+
T as default
|
|
63
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const u = {
|
|
2
|
+
name: "dom-primitive-bindings",
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
}, c = (t, n = u) => {
|
|
5
|
+
if (!(!t || typeof globalThis > "u"))
|
|
6
|
+
return globalThis.ks ?? (globalThis.ks = {}), globalThis.ks.domPrimitiveBindings = {
|
|
7
|
+
meta: n,
|
|
8
|
+
bindEvents: t
|
|
9
|
+
}, globalThis.ks.domPrimitiveBindings;
|
|
10
|
+
}, l = (t) => typeof t == "string" ? document.getElementById(t) || document.querySelector(t) : t && typeof t == "object" && "addEventListener" in t ? t : null, a = (t) => {
|
|
11
|
+
if (!t) return null;
|
|
12
|
+
const n = l(t);
|
|
13
|
+
return n || null;
|
|
14
|
+
}, f = (t) => Array.isArray(t) ? t.filter((n) => n && typeof n == "object" && typeof n.event == "string" && typeof n.selector == "string") : [], g = (t) => {
|
|
15
|
+
const n = f(t);
|
|
16
|
+
return n.length === 0 ? null : n;
|
|
17
|
+
}, p = (t, n) => {
|
|
18
|
+
const e = a(t);
|
|
19
|
+
if (!e) return null;
|
|
20
|
+
const r = g(n);
|
|
21
|
+
return r ? { container: e, bindings: r } : null;
|
|
22
|
+
}, d = (t) => {
|
|
23
|
+
const n = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const e of t) {
|
|
25
|
+
const r = n.get(e.event) ?? [];
|
|
26
|
+
r.push(e), n.set(e.event, r);
|
|
27
|
+
}
|
|
28
|
+
return n;
|
|
29
|
+
}, y = (t) => d(t), m = (t, n, e) => {
|
|
30
|
+
var r;
|
|
31
|
+
return t.root === "event" || (r = n == null ? void 0 : n.type) != null && r.startsWith("key") ? n : e;
|
|
32
|
+
}, h = (t, n, e) => ({
|
|
33
|
+
...t.clientData,
|
|
34
|
+
event: n,
|
|
35
|
+
targetElement: e
|
|
36
|
+
}), v = ({ binding: t, event: n, matchElement: e }) => {
|
|
37
|
+
const r = m(t, n, e), i = h(t, n, e), o = t.runner, s = typeof o == "function" ? o(r, t.instructions, i) : r;
|
|
38
|
+
return s != null && typeof t.onExtract == "function" && t.onExtract(s, n, e), s;
|
|
39
|
+
}, B = ({ bindings: t, event: n, container: e }) => {
|
|
40
|
+
var r, i;
|
|
41
|
+
for (const o of t) {
|
|
42
|
+
const s = (i = (r = n.target) == null ? void 0 : r.closest) == null ? void 0 : i.call(r, o.selector);
|
|
43
|
+
!s || !e.contains(s) || v({ binding: o, event: n, matchElement: s });
|
|
44
|
+
}
|
|
45
|
+
}, E = (t, n, e) => {
|
|
46
|
+
t.addEventListener(n, (r) => {
|
|
47
|
+
B({ bindings: e, event: r, container: t });
|
|
48
|
+
});
|
|
49
|
+
}, k = (t, n) => {
|
|
50
|
+
for (const [e, r] of n.entries())
|
|
51
|
+
E(t, e, r);
|
|
52
|
+
return t;
|
|
53
|
+
}, T = (t, n = []) => {
|
|
54
|
+
const e = p(t, n);
|
|
55
|
+
if (!e) return null;
|
|
56
|
+
const { container: r, bindings: i } = e, o = y(i);
|
|
57
|
+
return k(r, o), r;
|
|
58
|
+
};
|
|
59
|
+
c(T);
|
|
60
|
+
export {
|
|
61
|
+
T as bindEvents,
|
|
62
|
+
T as default
|
|
63
|
+
};
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>DOM Primitive Bindings (v1) — Architecture & Story</title>
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
background-color: #f8fafc;
|
|
13
|
+
color: #334155;
|
|
14
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
15
|
+
line-height: 1.6;
|
|
16
|
+
}
|
|
17
|
+
.hero-banner {
|
|
18
|
+
background: linear-gradient(135deg, #065f46 0%, #047857 100%);
|
|
19
|
+
color: #ffffff;
|
|
20
|
+
padding: 56px 0;
|
|
21
|
+
margin-bottom: 40px;
|
|
22
|
+
}
|
|
23
|
+
.feature-card {
|
|
24
|
+
background: #ffffff;
|
|
25
|
+
border: 1px solid #e2e8f0;
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
padding: 24px;
|
|
28
|
+
height: 100%;
|
|
29
|
+
}
|
|
30
|
+
pre {
|
|
31
|
+
background-color: #0f172a;
|
|
32
|
+
color: #f1f5f9;
|
|
33
|
+
padding: 16px;
|
|
34
|
+
border-radius: 8px;
|
|
35
|
+
font-size: 0.9rem;
|
|
36
|
+
overflow-x: auto;
|
|
37
|
+
}
|
|
38
|
+
code:not(pre code) {
|
|
39
|
+
background-color: #e2e8f0;
|
|
40
|
+
color: #0f172a;
|
|
41
|
+
padding: 2px 6px;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
font-size: 0.88em;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
|
|
48
|
+
<body>
|
|
49
|
+
|
|
50
|
+
<header class="hero-banner shadow-sm">
|
|
51
|
+
<div class="container">
|
|
52
|
+
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
|
53
|
+
<div>
|
|
54
|
+
<span class="badge bg-warning text-dark mb-2">v1 · Delegated Event Engine</span>
|
|
55
|
+
<h1 class="display-6 fw-bold mb-2">DOM Primitive Bindings</h1>
|
|
56
|
+
<p class="lead mb-0 text-white opacity-75">
|
|
57
|
+
Single-listener container delegation with zero overhead. Modern, declarative, and dynamic.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="d-flex gap-2">
|
|
61
|
+
<a href="../samples/v1/index.html" class="btn btn-outline-light btn-sm">
|
|
62
|
+
<i class="bi bi-play-circle me-1"></i> Interactive Samples
|
|
63
|
+
</a>
|
|
64
|
+
<a href="../index.html" class="btn btn-light btn-sm text-dark">
|
|
65
|
+
<i class="bi bi-house me-1"></i> Home
|
|
66
|
+
</a>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<main class="container pb-5" style="max-width: 1000px;">
|
|
73
|
+
|
|
74
|
+
<!-- Section 1: The Core Mental Model -->
|
|
75
|
+
<section class="card shadow-sm border-0 p-4 mb-4">
|
|
76
|
+
<h2 class="h5 fw-bold text-dark mb-3">
|
|
77
|
+
<i class="bi bi-diagram-3-fill text-success me-2"></i> The Event Delegation Story
|
|
78
|
+
</h2>
|
|
79
|
+
<p>
|
|
80
|
+
Traditional web apps attach <code>addEventListener</code> to individual buttons, inputs, and rows. When rows are re-rendered, paginated, or filtered, listeners are either lost or accidentally duplicated, creating memory leaks and stale event handlers.
|
|
81
|
+
</p>
|
|
82
|
+
<p class="mb-0">
|
|
83
|
+
<strong>DOM Primitive Bindings</strong> inverts this pattern. Exactly <strong>one listener per event type</strong> is attached to the parent container (e.g. <code>#table-container</code> or <code>#form-container</code>). When an event occurs, it bubbles to the container, checks for matched selectors via <code>closest(selector)</code>, and dispatches to the registered runner. New DOM elements added at runtime work immediately with zero re-binding!
|
|
84
|
+
</p>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<!-- Section 2: Staged Architecture -->
|
|
88
|
+
<section class="card shadow-sm border-0 p-4 mb-4">
|
|
89
|
+
<h2 class="h5 fw-bold text-dark mb-3">
|
|
90
|
+
<i class="bi bi-layers-fill text-primary me-2"></i> Staged Architecture in <code>src/v1/</code>
|
|
91
|
+
</h2>
|
|
92
|
+
|
|
93
|
+
<div class="row g-3">
|
|
94
|
+
<div class="col-md-6">
|
|
95
|
+
<div class="feature-card">
|
|
96
|
+
<h6 class="fw-bold text-success mb-2">
|
|
97
|
+
<i class="bi bi-shield-check me-1"></i> 1. guards/
|
|
98
|
+
</h6>
|
|
99
|
+
<p class="small text-muted mb-0">
|
|
100
|
+
Validates inputs before any DOM mutation occurs. Separated into <code>container/</code> (validates first input) and <code>bindings/</code> (validates second input array).
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="col-md-6">
|
|
106
|
+
<div class="feature-card">
|
|
107
|
+
<h6 class="fw-bold text-info mb-2">
|
|
108
|
+
<i class="bi bi-collection me-1"></i> 2. prepare/
|
|
109
|
+
</h6>
|
|
110
|
+
<p class="small text-muted mb-0">
|
|
111
|
+
Groups bindings by event type into a <code>Map<eventType, bindings[]></code>, ensuring only a single listener is registered per event type.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="col-md-6">
|
|
117
|
+
<div class="feature-card">
|
|
118
|
+
<h6 class="fw-bold text-primary mb-2">
|
|
119
|
+
<i class="bi bi-earbuds me-1"></i> 3. listeners/
|
|
120
|
+
</h6>
|
|
121
|
+
<p class="small text-muted mb-0">
|
|
122
|
+
Attaches container event listeners that intercept bubbling browser events and pass them down into the dispatch pipeline.
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="col-md-6">
|
|
128
|
+
<div class="feature-card">
|
|
129
|
+
<h6 class="fw-bold text-warning mb-2">
|
|
130
|
+
<i class="bi bi-lightning-charge me-1"></i> 4. dispatch/
|
|
131
|
+
</h6>
|
|
132
|
+
<p class="small text-muted mb-0">
|
|
133
|
+
Finds the matching target via <code>closest(selector)</code>, resolves start inputs, executes the runner, and emits extracted data to <code>onExtract</code>.
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
<!-- Section 3: Browser API & Window.ks -->
|
|
141
|
+
<section class="card shadow-sm border-0 p-4 mb-4">
|
|
142
|
+
<h2 class="h5 fw-bold text-dark mb-2">
|
|
143
|
+
<i class="bi bi-globe2 text-info me-2"></i> Browser Usage via <code>window.ks</code>
|
|
144
|
+
</h2>
|
|
145
|
+
<p class="small text-muted mb-3">
|
|
146
|
+
Include the bundled distribution or ES module, then call <code>bindEvents</code>:
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
<pre class="mb-3"><code><script type="module" src="/docs/dist/v1/min.js"></script>
|
|
150
|
+
<script type="module">
|
|
151
|
+
const bindings = [
|
|
152
|
+
{
|
|
153
|
+
event: "click",
|
|
154
|
+
selector: ".btn-delete",
|
|
155
|
+
runner: (target) => ({ id: target.dataset.id }),
|
|
156
|
+
onExtract: (data) => console.log("Deleted:", data)
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
window.ks.domPrimitiveBindings.bindEvents("container-id", bindings);
|
|
161
|
+
</script></code></pre>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<!-- Section 4: Design Rules & Zero Comments -->
|
|
165
|
+
<section class="card shadow-sm border-0 p-4">
|
|
166
|
+
<h2 class="h5 fw-bold text-dark mb-3">
|
|
167
|
+
<i class="bi bi-check2-circle text-success me-2"></i> Code Conventions
|
|
168
|
+
</h2>
|
|
169
|
+
<ul class="list-unstyled mb-0 small text-muted">
|
|
170
|
+
<li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Every folder has an <code>index.js</code></strong> file acting as its public gateway.</li>
|
|
171
|
+
<li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Single-function default export</strong> per file (no default export objects).</li>
|
|
172
|
+
<li class="mb-2"><i class="bi bi-check-lg text-success me-2"></i><strong>Zero comments in code:</strong> Folders and file names tell the story directly.</li>
|
|
173
|
+
<li><i class="bi bi-check-lg text-success me-2"></i><strong>Independent sibling package:</strong> Focused purely on event delegation without mixing extraction concerns.</li>
|
|
174
|
+
</ul>
|
|
175
|
+
</section>
|
|
176
|
+
|
|
177
|
+
</main>
|
|
178
|
+
|
|
179
|
+
<footer class="text-center text-muted py-4 border-top small">
|
|
180
|
+
DOM Primitive Bindings · Single Source of Truth: <code>v1</code> · MIT License
|
|
181
|
+
</footer>
|
|
182
|
+
|
|
183
|
+
</body>
|
|
184
|
+
|
|
185
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dom-primitive-bindings",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Declarative, instruction-driven DOM delegated event bindings",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "KeshavSoft",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/keshavsoft/dom-primitive-bindings.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/keshavsoft/dom-primitive-bindings/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/keshavsoft/dom-primitive-bindings#readme",
|
|
16
|
+
"main": "./src/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src/index.js",
|
|
22
|
+
"src/v1",
|
|
23
|
+
"docs",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "vite",
|
|
29
|
+
"build": "vite build",
|
|
30
|
+
"preview": "vite preview",
|
|
31
|
+
"test": "node --test"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"vite": "^6.4.1"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import getStartInput from "./getStartInput.js";
|
|
2
|
+
import getClientData from "./getClientData.js";
|
|
3
|
+
|
|
4
|
+
const executeBinding = ({ binding, event, matchElement }) => {
|
|
5
|
+
const startInput = getStartInput(binding, event, matchElement);
|
|
6
|
+
const clientData = getClientData(binding, event, matchElement);
|
|
7
|
+
const runner = binding.runner;
|
|
8
|
+
|
|
9
|
+
const result = typeof runner === "function"
|
|
10
|
+
? runner(startInput, binding.instructions, clientData)
|
|
11
|
+
: startInput;
|
|
12
|
+
|
|
13
|
+
if (result !== null && result !== undefined && typeof binding.onExtract === "function") {
|
|
14
|
+
binding.onExtract(result, event, matchElement);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default executeBinding;
|
|
21
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import executeBinding from "./executeBinding.js";
|
|
2
|
+
|
|
3
|
+
const dispatchEvent = ({ bindings, event, container }) => {
|
|
4
|
+
for (const binding of bindings) {
|
|
5
|
+
const matchElement = event.target?.closest?.(binding.selector);
|
|
6
|
+
if (!matchElement || !container.contains(matchElement)) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
executeBinding({ binding, event, matchElement });
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default dispatchEvent;
|
|
15
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const validateArray = (bindings) => {
|
|
2
|
+
if (!Array.isArray(bindings)) return [];
|
|
3
|
+
|
|
4
|
+
return bindings.filter((binding) => (
|
|
5
|
+
binding && typeof binding === "object" && typeof binding.event === "string" && typeof binding.selector === "string"
|
|
6
|
+
));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default validateArray;
|
|
10
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import resolveElement from "./resolveElement.js";
|
|
2
|
+
|
|
3
|
+
const checkContainer = (containerOrId) => {
|
|
4
|
+
if (!containerOrId) return null;
|
|
5
|
+
|
|
6
|
+
const element = resolveElement(containerOrId);
|
|
7
|
+
if (!element) return null;
|
|
8
|
+
|
|
9
|
+
return element;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default checkContainer;
|
|
13
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const resolveElement = (containerOrId) => {
|
|
2
|
+
if (typeof containerOrId === "string") {
|
|
3
|
+
return document.getElementById(containerOrId) || document.querySelector(containerOrId);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (containerOrId && typeof containerOrId === "object" && "addEventListener" in containerOrId) {
|
|
7
|
+
return containerOrId;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default resolveElement;
|
|
14
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import checkContainer from "./container/index.js";
|
|
2
|
+
import checkBindings from "./bindings/index.js";
|
|
3
|
+
|
|
4
|
+
const runGuards = (containerOrId, bindings) => {
|
|
5
|
+
const container = checkContainer(containerOrId);
|
|
6
|
+
if (!container) return null;
|
|
7
|
+
|
|
8
|
+
const validBindings = checkBindings(bindings);
|
|
9
|
+
if (!validBindings) return null;
|
|
10
|
+
|
|
11
|
+
return { container, bindings: validBindings };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default runGuards;
|
|
15
|
+
|
package/src/v1/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import registerGlobal from "./register/index.js";
|
|
2
|
+
import runGuards from "./guards/index.js";
|
|
3
|
+
import prepareBindings from "./prepare/index.js";
|
|
4
|
+
import attachListeners from "./listeners/index.js";
|
|
5
|
+
|
|
6
|
+
const bindEvents = (containerOrId, bindings = []) => {
|
|
7
|
+
const guarded = runGuards(containerOrId, bindings);
|
|
8
|
+
if (!guarded) return null;
|
|
9
|
+
|
|
10
|
+
const { container, bindings: validBindings } = guarded;
|
|
11
|
+
const eventGroups = prepareBindings(validBindings);
|
|
12
|
+
|
|
13
|
+
attachListeners(container, eventGroups);
|
|
14
|
+
|
|
15
|
+
return container;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { bindEvents };
|
|
19
|
+
export default bindEvents;
|
|
20
|
+
|
|
21
|
+
registerGlobal(bindEvents);
|
|
22
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import dispatchEvent from "../dispatch/index.js";
|
|
2
|
+
|
|
3
|
+
const attachListener = (container, eventType, bindings) => {
|
|
4
|
+
container.addEventListener(eventType, (event) => {
|
|
5
|
+
dispatchEvent({ bindings, event, container });
|
|
6
|
+
});
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default attachListener;
|
|
10
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import attachListener from "./attachListener.js";
|
|
2
|
+
|
|
3
|
+
const attachListeners = (container, eventGroups) => {
|
|
4
|
+
for (const [eventType, bindings] of eventGroups.entries()) {
|
|
5
|
+
attachListener(container, eventType, bindings);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return container;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default attachListeners;
|
|
12
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const groupBindings = (bindings) => {
|
|
2
|
+
const groups = new Map();
|
|
3
|
+
|
|
4
|
+
for (const binding of bindings) {
|
|
5
|
+
const list = groups.get(binding.event) ?? [];
|
|
6
|
+
list.push(binding);
|
|
7
|
+
groups.set(binding.event, list);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return groups;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default groupBindings;
|
|
14
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import meta from "./meta.js";
|
|
2
|
+
|
|
3
|
+
const registerGlobal = (bindEvents, metadata = meta) => {
|
|
4
|
+
if (!bindEvents || typeof globalThis === "undefined") return;
|
|
5
|
+
|
|
6
|
+
globalThis.ks ??= {};
|
|
7
|
+
|
|
8
|
+
globalThis.ks.domPrimitiveBindings = {
|
|
9
|
+
meta: metadata,
|
|
10
|
+
bindEvents
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return globalThis.ks.domPrimitiveBindings;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default registerGlobal;
|
|
17
|
+
|