lime-csr-js 0.1.4
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/DOCS.md +1456 -0
- package/LICENCE.md +20 -0
- package/README.md +150 -0
- package/dist/index.min.js +1 -0
- package/package.json +53 -0
- package/src/bindings-blocks.js +363 -0
- package/src/bindings-events.js +176 -0
- package/src/bindings-loops.js +568 -0
- package/src/bindings-model.js +262 -0
- package/src/bindings-show.js +97 -0
- package/src/bindings.js +240 -0
- package/src/conditionals.js +153 -0
- package/src/errors.js +414 -0
- package/src/index.js +312 -0
- package/src/loops.js +117 -0
- package/src/partials.js +158 -0
- package/src/shared.js +103 -0
- package/src/store.js +265 -0
- package/src/template.js +263 -0
- package/src/utils.js +84 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module bindings-events
|
|
3
|
+
* Event shortcut — `data-on-{event}="handlerName"` (the EVAL-FREE
|
|
4
|
+
* counterpart of Alpine's `x-on`).
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT DOES
|
|
7
|
+
* <button data-on-click="addComment">Add</button>
|
|
8
|
+
* A HANDLER DICTIONARY is passed to the mount() call:
|
|
9
|
+
* mount("page", ctx, target, store, { handlers: { addComment(e, el) {...} } })
|
|
10
|
+
* On click, the framework looks up "addComment" by name in the dictionary and calls it.
|
|
11
|
+
*
|
|
12
|
+
* NO EVAL — NAME MATCHING (a DELIBERATE DIFFERENCE from Alpine):
|
|
13
|
+
* In Alpine, `x-on:click="count++"` is a JS EXPRESSION in the attribute
|
|
14
|
+
* value, and it gets executed (via a `new Function`-like mechanism). Here
|
|
15
|
+
* the attribute value ("addComment") is NOT an expression — it's only a
|
|
16
|
+
* KEY (string) looked up in the `handlers` dictionary. No code is ever
|
|
17
|
+
* generated from a string and executed; this is fully consistent with the
|
|
18
|
+
* project-wide "no new Function/eval" principle (see the same principle in
|
|
19
|
+
* template.js/conditionals.js).
|
|
20
|
+
*
|
|
21
|
+
* SUPPORTED EVENTS (SUPPORTED_EVENTS):
|
|
22
|
+
* click, input, change, submit, keydown — derived from the attribute name
|
|
23
|
+
* (`data-on-{event}`). If a type outside this list of 5 is used (e.g.
|
|
24
|
+
* `data-on-foo`), a dev-mode warning is issued and that attribute is ignored.
|
|
25
|
+
*
|
|
26
|
+
* MECHANISM — DELEGATION (not one listener per element):
|
|
27
|
+
* setupEventBindings sets up ONE listener PER USED EVENT TYPE
|
|
28
|
+
* (`root.addEventListener(type, ...)`), not per element — and only for the
|
|
29
|
+
* types actually used, not blindly for all 5 (see collectUsedEventTypes).
|
|
30
|
+
* When an event arrives, the real target is found via
|
|
31
|
+
* `event.target.closest('[data-on-{type}]')`, the handler name is read
|
|
32
|
+
* from the attribute, and looked up in the dictionary.
|
|
33
|
+
*
|
|
34
|
+
* ADVANTAGE OF DELEGATION: when reactive `<for data-live>`/`<if data-live>`
|
|
35
|
+
* adds new content and removes old content, there is NO NEED to set up a
|
|
36
|
+
* separate listener for those new elements — the single listener on `root`
|
|
37
|
+
* always catches them via event bubbling. Since `root` is a FIXED root for
|
|
38
|
+
* the duration of mount() that is UNAFFECTED by these reactive internal
|
|
39
|
+
* changes (only the nodes INSIDE it change, `root` itself never changes),
|
|
40
|
+
* this guarantee holds permanently — see the "delegation proof" test in
|
|
41
|
+
* examples/events-test.html.
|
|
42
|
+
*
|
|
43
|
+
* WHY root, NOT document: if the listener were attached to `document`, then
|
|
44
|
+
* with multiple mount() calls on the page, every `document` listener would
|
|
45
|
+
* listen to the ENTIRE page — one mount's handlers dictionary could
|
|
46
|
+
* mistakenly catch a data-on-* element belonging to another mount (which
|
|
47
|
+
* could have the same name). Using `root` (mount()'s `target`) isolates
|
|
48
|
+
* each mount's own delegation; the `root.contains(el)` check is an extra
|
|
49
|
+
* layer of guarantee.
|
|
50
|
+
*
|
|
51
|
+
* NO inLiveBlock EXCEPTION NEEDED (DIFFERENCE from bindings.js/bindings-model.js/bindings-show.js):
|
|
52
|
+
* Those modules walk every element ONE BY ONE AT RENDER TIME (before the
|
|
53
|
+
* fragment is added to the DOM) and set up a subscription — if they bound
|
|
54
|
+
* an element inside a not-yet-expanded live block too early, that
|
|
55
|
+
* subscription would leak when the block later re-renders via its own
|
|
56
|
+
* renderFn (staying attached to the old DOM). Event delegation, on the
|
|
57
|
+
* other hand, never binds any element individually — a single root-level
|
|
58
|
+
* listener is set up, and when an event is ACTUALLY CLICKED, closest() is
|
|
59
|
+
* used to search the CURRENT (up-to-date) DOM at that moment. The risk of
|
|
60
|
+
* "binding too early to the wrong context" is structurally absent; hence
|
|
61
|
+
* no need for the inLiveBlock filter at all.
|
|
62
|
+
*
|
|
63
|
+
* MODIFIER — ONLY data-on-submit → preventDefault (DECISION):
|
|
64
|
+
* Not reloading the page on form submit is the OVERWHELMINGLY common
|
|
65
|
+
* desired behavior; so data-on-submit ALWAYS calls preventDefault (no
|
|
66
|
+
* opt-in data-prevent attribute was deemed NECESSARY — KISS). preventDefault
|
|
67
|
+
* is applied EVEN IF the handler is not found (a typo): otherwise the
|
|
68
|
+
* form's native submit would kick in and reload the page — exactly what we
|
|
69
|
+
* want to prevent. Other modifiers (stop propagation, once, debounce) were
|
|
70
|
+
* NOT ADDED — TODO: could be addressed later via a separate data-*
|
|
71
|
+
* attribute (e.g. `data-once`).
|
|
72
|
+
*
|
|
73
|
+
* SCOPE DECISION — no context is INJECTED into the handler:
|
|
74
|
+
* Handlers are written in APPLICATION code and already have access to
|
|
75
|
+
* their own store reference (via closure); the framework doesn't need to
|
|
76
|
+
* pass a context separately. Only `(event, element)` is passed to the
|
|
77
|
+
* handler — `element.dataset` is used to find out which item it relates to
|
|
78
|
+
* (e.g. `data-id="42"` → `el.dataset.id`; see the "Events" section in README).
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import { errors } from './errors.js';
|
|
82
|
+
|
|
83
|
+
/** @type {Set<string>} Event types supported as data-on-{event}. */
|
|
84
|
+
const SUPPORTED_EVENTS = new Set(['click', 'input', 'change', 'submit', 'keydown']);
|
|
85
|
+
|
|
86
|
+
const EVENT_ATTR_PATTERN = /^data-on-(.+)$/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Scans ALL <template> contents on the page and collects the data-on-{event}
|
|
90
|
+
* types actually in use (intersected with SUPPORTED_EVENTS).
|
|
91
|
+
*
|
|
92
|
+
* WHY "ALL templates" (not just THIS mount's template):
|
|
93
|
+
* An <if data-live>'s else branch, a <for data-live>'s currently-empty loop
|
|
94
|
+
* body, or a <partial>'s OWN template may not YET be VISIBLE in the live
|
|
95
|
+
* DOM at mount TIME (they only appear once the relevant branch/item/partial
|
|
96
|
+
* is rendered). So scanning only the CURRENTLY rendered DOM would be
|
|
97
|
+
* insufficient — a data-on-* type that gets added LATER via reactivity
|
|
98
|
+
* would never have gotten a listener set up for it. The raw <template>
|
|
99
|
+
* sources (document.querySelectorAll('template')), however, are NEVER
|
|
100
|
+
* mutated (see template.js: the cache always returns cloneNode(true)) — so
|
|
101
|
+
* as written, regardless of which branch/loop/partial they belong to, they
|
|
102
|
+
* safely surface ALL data-on-* usages. If an unknown (outside
|
|
103
|
+
* SUPPORTED_EVENTS) type is found, it's warned about here (once, at scan time).
|
|
104
|
+
*
|
|
105
|
+
* @returns {Set<string>}
|
|
106
|
+
*/
|
|
107
|
+
function collectUsedEventTypes() {
|
|
108
|
+
const types = new Set();
|
|
109
|
+
|
|
110
|
+
for (const tpl of document.querySelectorAll('template')) {
|
|
111
|
+
for (const el of tpl.content.querySelectorAll('*')) {
|
|
112
|
+
for (const attr of el.attributes) {
|
|
113
|
+
const match = EVENT_ATTR_PATTERN.exec(attr.name);
|
|
114
|
+
if (!match) continue;
|
|
115
|
+
|
|
116
|
+
const eventName = match[1];
|
|
117
|
+
if (SUPPORTED_EVENTS.has(eventName)) {
|
|
118
|
+
types.add(eventName);
|
|
119
|
+
} else {
|
|
120
|
+
errors.unknownEvent(eventName, Array.from(SUPPORTED_EVENTS), el);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return types;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets up a SINGLE delegation listener on root for each data-on-{event} type
|
|
131
|
+
* actually in use. A handler call whose name isn't found in the handlers
|
|
132
|
+
* dictionary is warned about in dev-mode (no crash).
|
|
133
|
+
*
|
|
134
|
+
* @param {Element} root - Delegation root (mount()'s target)
|
|
135
|
+
* @param {import('./store.js').Store} store - Currently UNUSED (no context is
|
|
136
|
+
* injected into handlers, see the module JSDoc — kept for signature
|
|
137
|
+
* consistency with the other setup*Bindings functions).
|
|
138
|
+
* @param {Object<string, function(Event, Element): void>} handlers
|
|
139
|
+
* @returns {function(): void} cleanup — removes all listeners that were set up
|
|
140
|
+
*/
|
|
141
|
+
export function setupEventBindings(root, store, handlers) {
|
|
142
|
+
const usedTypes = collectUsedEventTypes();
|
|
143
|
+
const listeners = [];
|
|
144
|
+
|
|
145
|
+
for (const type of usedTypes) {
|
|
146
|
+
const attrName = `data-on-${type}`;
|
|
147
|
+
|
|
148
|
+
const onEvent = (event) => {
|
|
149
|
+
const el = event.target.closest?.(`[${attrName}]`);
|
|
150
|
+
if (!el || !root.contains(el)) return;
|
|
151
|
+
|
|
152
|
+
// data-on-submit ALWAYS calls preventDefault (see the "MODIFIER"
|
|
153
|
+
// section in the module JSDoc) — even if the handler isn't found, to
|
|
154
|
+
// prevent the native submit from reloading the page.
|
|
155
|
+
if (type === 'submit') event.preventDefault();
|
|
156
|
+
|
|
157
|
+
const handlerName = el.getAttribute(attrName);
|
|
158
|
+
const handler = Object.hasOwn(handlers, handlerName) ? handlers[handlerName] : undefined;
|
|
159
|
+
|
|
160
|
+
if (!handler) {
|
|
161
|
+
errors.handlerNotFound(handlerName, Object.keys(handlers), el);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
handler(event, el);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
root.addEventListener(type, onEvent);
|
|
169
|
+
listeners.push({ type, onEvent });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return function cleanup() {
|
|
173
|
+
for (const { type, onEvent } of listeners) root.removeEventListener(type, onEvent);
|
|
174
|
+
listeners.length = 0; // guard against double cleanup calls
|
|
175
|
+
};
|
|
176
|
+
}
|