@daz4126/helium 0.5.2 → 0.6.1

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 +1 -3
  2. package/helium.js +176 -72
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,11 +14,10 @@ It's really simple to use - just sprinkle the magic @attributes into your HTML a
14
14
 
15
15
  [See more examples here](https://codepen.io/daz4126/pen/YPwwdBK)
16
16
 
17
- To use, just import from the CDN then call the `helium` funtion (no install or build step required!):
17
+ To use, just import from the CDN (no install or build step required!):
18
18
 
19
19
  ```javascript
20
20
  import helium from "https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js"
21
- helium()
22
21
  ```
23
22
 
24
23
  Alernatively you can install from NPM:
@@ -31,7 +30,6 @@ Then include it in your
31
30
 
32
31
  ```javascript
33
32
  import helium from "@daz4126/helium"
34
- helium()
35
33
  ```
36
34
 
37
35
  # Helium Attributes
package/helium.js CHANGED
@@ -3,6 +3,10 @@ export default function helium(data = {}) {
3
3
  document.querySelector("[\\@helium]") ||
4
4
  document.querySelector("[data-helium]") ||
5
5
  document.body;
6
+ if (Object.keys(data).length > 0)
7
+ document
8
+ .querySelectorAll("[data-helium-processed]")
9
+ .forEach((el) => el.removeAttribute("data-helium-processed"));
6
10
  const [bindings, refs] = [new Map(), new Map()];
7
11
  const $ = (selector) => document.querySelector(selector);
8
12
  const html = (string) => {
@@ -10,17 +14,43 @@ export default function helium(data = {}) {
10
14
  temp.innerHTML = string;
11
15
  return temp.content.firstChild;
12
16
  };
17
+
18
+ const ajax = (url, method, target, options) => {
19
+ fetch(url, {
20
+ method,
21
+ headers: { "Content-Type": "application/json" },
22
+ body: method === "GET" ? null : JSON.stringify(options),
23
+ })
24
+ .then((res) =>
25
+ res.headers.get("content-type")?.includes("application/json")
26
+ ? res.json()
27
+ : res.text(),
28
+ )
29
+ .then((data) => (state[target] = data));
30
+ };
31
+ const get = (url, prop) => ajax(url, "GET", prop);
32
+ const post = (url, params, prop) => ajax(url, "POST", prop, params);
33
+ const patch = (url, params, prop) => ajax(url, "PATCH", prop, params);
34
+ const del = (url, params, prop) => ajax(url, "DELETE", prop, params);
35
+
13
36
  const state = new Proxy(data, {
14
37
  get(target, prop) {
15
38
  return target[prop];
16
39
  },
17
40
  set(target, prop, value) {
41
+ // skip if identical (prevents redundant re-renders)
42
+ if (target[prop] === value) return true;
18
43
  target[prop] = value;
19
- if (bindings.has(prop))
44
+ if (bindings.has(prop)) {
45
+ // only call the bindings for this prop
20
46
  bindings.get(prop).forEach((binding) => applyBinding(binding));
47
+ }
21
48
  return true;
22
49
  },
23
50
  });
51
+
52
+ let isUpdatingDOM = false;
53
+
24
54
  function applyBinding(binding, event = {}, elCtx = binding.el) {
25
55
  const { el, prop, fn } = binding;
26
56
  const result = fn(
@@ -29,12 +59,25 @@ export default function helium(data = {}) {
29
59
  event,
30
60
  elCtx,
31
61
  html,
32
- ...Object.values(state),
62
+ get,
63
+ post,
64
+ patch,
65
+ del,
66
+ ...Object.values(data),
33
67
  ...[...refs.values()],
34
68
  );
35
- if (prop in el) el[prop] = result;
36
- else el.setAttribute(prop, result);
69
+ // guard to reduce observer recursion (optional but helpful)
70
+ if (prop === "innerHTML") {
71
+ isUpdatingDOM = true;
72
+ el.innerHTML = result;
73
+ isUpdatingDOM = false;
74
+ } else if (prop in el) {
75
+ el[prop] = result;
76
+ } else {
77
+ el.setAttribute(prop, result);
78
+ }
37
79
  }
80
+
38
81
  function compileExpression(expr, withReturn = false) {
39
82
  try {
40
83
  return new Function(
@@ -43,6 +86,10 @@ export default function helium(data = {}) {
43
86
  "$event",
44
87
  "$el",
45
88
  "$html",
89
+ "$get",
90
+ "$post",
91
+ "$patch",
92
+ "$delete",
46
93
  ...Object.keys(data),
47
94
  ...[...refs.keys()],
48
95
  `with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
@@ -51,7 +98,21 @@ export default function helium(data = {}) {
51
98
  return () => expr;
52
99
  }
53
100
  }
101
+
102
+ // processElements now returns an array of newly-created binding objects
54
103
  function processElements(element) {
104
+ const newlyAddedBindings = [];
105
+
106
+ // quick guard: avoid re-processing same node
107
+ if (
108
+ element.nodeType === 1 &&
109
+ element.hasAttribute &&
110
+ element.hasAttribute("data-helium-processed")
111
+ )
112
+ return newlyAddedBindings;
113
+ if (element.nodeType === 1 && element.setAttribute)
114
+ element.setAttribute("data-helium-processed", "");
115
+
55
116
  const heliumElements = [
56
117
  element,
57
118
  ...Array.from(element.querySelectorAll("*")).filter((el) =>
@@ -60,6 +121,7 @@ export default function helium(data = {}) {
60
121
  ),
61
122
  ),
62
123
  ];
124
+
63
125
  const xpath = document.evaluate(
64
126
  ".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
65
127
  element,
@@ -69,8 +131,10 @@ export default function helium(data = {}) {
69
131
  );
70
132
  for (let i = 0; i < xpath.snapshotLength; i++)
71
133
  heliumElements.push(xpath.snapshotItem(i));
134
+
135
+ // seed default state values (no binding creation here)
72
136
  heliumElements.forEach((el) => {
73
- for (const { name, value } of el.attributes) {
137
+ for (const { name, value } of el.attributes || []) {
74
138
  if (
75
139
  name == "@text" ||
76
140
  name == "data-he-text" ||
@@ -87,9 +151,11 @@ export default function helium(data = {}) {
87
151
  }
88
152
  }
89
153
  });
154
+
155
+ // Register bindings for these elements and collect the newly added binding objects
90
156
  heliumElements.forEach((el) => {
91
- for (const { name, value } of el.attributes) {
92
- if (name == "@data" || name == "data-he")
157
+ for (const { name, value } of el.attributes || []) {
158
+ if (name == "@data" || name == "data-he") {
93
159
  Object.assign(
94
160
  state,
95
161
  compileExpression(value, true)(
@@ -98,85 +164,107 @@ export default function helium(data = {}) {
98
164
  {},
99
165
  el,
100
166
  html,
101
- ...Object.values(state),
167
+ get,
168
+ post,
169
+ patch,
170
+ del,
171
+ ...Object.values(data),
102
172
  ...[...refs.values()],
103
173
  ),
104
174
  );
175
+ }
105
176
  if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
106
- if (name == "@text" || name == "data-he-text")
177
+
178
+ if (
179
+ name == "@text" ||
180
+ name == "data-he-text" ||
181
+ name == "@html" ||
182
+ name == "data-he-html"
183
+ ) {
107
184
  Object.keys(state)
108
185
  .filter((key) => value.includes(key))
109
- .forEach((val) =>
110
- bindings.set(val, [
111
- ...(bindings.get(val) || []),
112
- {
113
- el,
114
- prop: "textContent",
115
- expr: value,
116
- fn: compileExpression(value, true),
117
- },
118
- ]),
119
- );
186
+ .forEach((val) => {
187
+ const b = {
188
+ el,
189
+ prop:
190
+ name == "@text" || name == "data-he-text"
191
+ ? "textContent"
192
+ : "innerHTML",
193
+ expr: value,
194
+ fn: compileExpression(value, true),
195
+ };
196
+ bindings.set(val, [...(bindings.get(val) || []), b]);
197
+ newlyAddedBindings.push(b);
198
+ });
199
+ }
200
+
120
201
  if (name == "@bind" || name == "data-he-bind") {
121
202
  el.addEventListener("input", (e) => (state[value] = e.target.value));
122
- bindings.set(value, [
123
- ...(bindings.get(value) || []),
124
- {
125
- el,
126
- prop: "value",
127
- expr: value,
128
- fn: compileExpression(value, true),
129
- },
130
- ]);
203
+ const b = {
204
+ el,
205
+ prop: "value",
206
+ expr: value,
207
+ fn: compileExpression(value, true),
208
+ };
209
+ bindings.set(value, [...(bindings.get(value) || []), b]);
210
+ newlyAddedBindings.push(b);
131
211
  el.value = state[value];
132
212
  }
213
+
133
214
  if (
134
215
  name == "@hidden" ||
135
216
  name == "@visible" ||
136
217
  name == "data-he-hidden" ||
137
218
  name == "data-he-visible"
138
- )
219
+ ) {
139
220
  Object.keys(state)
140
221
  .filter((key) => value.includes(key))
141
- .forEach((val) =>
142
- bindings.set(val, [
143
- ...(bindings.get(val) || []),
144
- {
145
- el,
146
- prop: "hidden",
147
- expr: value,
148
- fn: compileExpression(
149
- `${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
150
- true,
151
- ),
152
- },
153
- ]),
154
- );
155
- if (name.startsWith(":"))
222
+ .forEach((val) => {
223
+ const b = {
224
+ el,
225
+ prop: "hidden",
226
+ expr: value,
227
+ fn: compileExpression(
228
+ `${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
229
+ true,
230
+ ),
231
+ };
232
+ bindings.set(val, [...(bindings.get(val) || []), b]);
233
+ newlyAddedBindings.push(b);
234
+ });
235
+ }
236
+
237
+ if (name.startsWith(":")) {
156
238
  Object.keys(state)
157
239
  .filter((key) => value.includes(key))
158
- .forEach((val) =>
159
- bindings.set(val, [
160
- ...(bindings.get(val) || []),
161
- {
162
- el,
163
- prop: name.split(":")[1],
164
- expr: value,
165
- fn: compileExpression(value, true),
166
- },
167
- ]),
168
- );
169
- if (name == "@init" || name == "data-he-init")
240
+ .forEach((val) => {
241
+ const b = {
242
+ el,
243
+ prop: name.split(":")[1],
244
+ expr: value,
245
+ fn: compileExpression(value, true),
246
+ };
247
+ bindings.set(val, [...(bindings.get(val) || []), b]);
248
+ newlyAddedBindings.push(b);
249
+ });
250
+ }
251
+
252
+ if (name == "@init" || name == "data-he-init") {
170
253
  compileExpression(value, false)(
171
254
  $,
172
255
  state,
173
256
  undefined,
174
257
  el,
175
258
  html,
176
- ...Object.values(state),
259
+ get,
260
+ post,
261
+ patch,
262
+ del,
263
+ ...Object.values(data),
177
264
  ...[...refs.values()],
178
265
  );
179
- else if (name.startsWith("@") || name.startsWith("data-he-on")) {
266
+ } else if (name.startsWith("@") || name.startsWith("data-he-on")) {
267
+ // event handling — keep this as before (register listeners)
180
268
  const [eventName, ...modifiers] = name
181
269
  .slice(name.startsWith("@") ? 1 : 10)
182
270
  .split(".");
@@ -230,7 +318,11 @@ export default function helium(data = {}) {
230
318
  e,
231
319
  el,
232
320
  html,
233
- ...Object.values(state),
321
+ get,
322
+ post,
323
+ patch,
324
+ del,
325
+ ...Object.values(data),
234
326
  ...[...refs.values()],
235
327
  );
236
328
  }
@@ -240,21 +332,33 @@ export default function helium(data = {}) {
240
332
 
241
333
  const debouncedHandler =
242
334
  debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
243
-
244
335
  receiver.addEventListener(eventName, debouncedHandler);
245
336
  }
246
- }
247
- });
248
- const observer = new MutationObserver((mutations) => {
249
- for (const m of mutations) {
250
- for (const node of m.addedNodes) {
251
- if (node.nodeType === 1) processElements(node);
337
+ } // end attributes loop
338
+ }); // end heliumElements.forEach
339
+
340
+ return newlyAddedBindings;
341
+ } // end processElements
342
+
343
+ // set up mutation observer that only processes and applies bindings for newly added nodes
344
+ const observer = new MutationObserver((mutations) => {
345
+ if (isUpdatingDOM) return; // ignore mutations we caused directly
346
+ for (const m of mutations) {
347
+ for (const node of m.addedNodes) {
348
+ if (
349
+ node.nodeType === 1 &&
350
+ !node.hasAttribute("data-helium-processed")
351
+ ) {
352
+ const newly = processElements(node);
353
+ // apply only the bindings we just created for this node
354
+ newly.forEach(applyBinding);
252
355
  }
253
356
  }
254
- });
255
- observer.observe(element, { childList: true, subtree: true });
256
- for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
257
- }
258
- processElements(root);
357
+ }
358
+ });
359
+ observer.observe(root, { childList: true, subtree: true });
360
+
361
+ const initialBindings = processElements(root);
362
+ for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
259
363
  }
260
364
  helium();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "keywords": [