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