@daz4126/helium 0.5.1 → 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 +171 -68
  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(
@@ -28,12 +54,26 @@ export default function helium(data = {}) {
28
54
  state,
29
55
  event,
30
56
  elCtx,
57
+ html,
58
+ get,
59
+ post,
60
+ patch,
61
+ del,
31
62
  ...Object.values(data),
32
63
  ...[...refs.values()],
33
64
  );
34
- if (prop in el) el[prop] = result;
35
- 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
+ }
36
75
  }
76
+
37
77
  function compileExpression(expr, withReturn = false) {
38
78
  try {
39
79
  return new Function(
@@ -42,6 +82,10 @@ export default function helium(data = {}) {
42
82
  "$event",
43
83
  "$el",
44
84
  "$html",
85
+ "$get",
86
+ "$post",
87
+ "$patch",
88
+ "$delete",
45
89
  ...Object.keys(data),
46
90
  ...[...refs.keys()],
47
91
  `with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
@@ -50,7 +94,23 @@ export default function helium(data = {}) {
50
94
  return () => expr;
51
95
  }
52
96
  }
97
+
98
+ // processElements now returns an array of newly-created binding objects
53
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
+
54
114
  const heliumElements = [
55
115
  element,
56
116
  ...Array.from(element.querySelectorAll("*")).filter((el) =>
@@ -59,6 +119,7 @@ export default function helium(data = {}) {
59
119
  ),
60
120
  ),
61
121
  ];
122
+
62
123
  const xpath = document.evaluate(
63
124
  ".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
64
125
  element,
@@ -68,8 +129,10 @@ export default function helium(data = {}) {
68
129
  );
69
130
  for (let i = 0; i < xpath.snapshotLength; i++)
70
131
  heliumElements.push(xpath.snapshotItem(i));
132
+
133
+ // seed default state values (no binding creation here)
71
134
  heliumElements.forEach((el) => {
72
- for (const { name, value } of el.attributes) {
135
+ for (const { name, value } of el.attributes || []) {
73
136
  if (
74
137
  name == "@text" ||
75
138
  name == "data-he-text" ||
@@ -86,9 +149,11 @@ export default function helium(data = {}) {
86
149
  }
87
150
  }
88
151
  });
152
+
153
+ // Register bindings for these elements and collect the newly added binding objects
89
154
  heliumElements.forEach((el) => {
90
- for (const { name, value } of el.attributes) {
91
- if (name == "@data" || name == "data-he")
155
+ for (const { name, value } of el.attributes || []) {
156
+ if (name == "@data" || name == "data-he") {
92
157
  Object.assign(
93
158
  state,
94
159
  compileExpression(value, true)(
@@ -97,85 +162,107 @@ export default function helium(data = {}) {
97
162
  {},
98
163
  el,
99
164
  html,
165
+ get,
166
+ post,
167
+ patch,
168
+ del,
100
169
  ...Object.values(data),
101
170
  ...[...refs.values()],
102
171
  ),
103
172
  );
173
+ }
104
174
  if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
105
- 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
+ ) {
106
182
  Object.keys(state)
107
183
  .filter((key) => value.includes(key))
108
- .forEach((val) =>
109
- bindings.set(val, [
110
- ...(bindings.get(val) || []),
111
- {
112
- el,
113
- prop: "textContent",
114
- expr: value,
115
- fn: compileExpression(value, true),
116
- },
117
- ]),
118
- );
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
+
119
199
  if (name == "@bind" || name == "data-he-bind") {
120
200
  el.addEventListener("input", (e) => (state[value] = e.target.value));
121
- bindings.set(value, [
122
- ...(bindings.get(value) || []),
123
- {
124
- el,
125
- prop: "value",
126
- expr: value,
127
- fn: compileExpression(value, true),
128
- },
129
- ]);
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);
130
209
  el.value = state[value];
131
210
  }
211
+
132
212
  if (
133
213
  name == "@hidden" ||
134
214
  name == "@visible" ||
135
215
  name == "data-he-hidden" ||
136
216
  name == "data-he-visible"
137
- )
217
+ ) {
138
218
  Object.keys(state)
139
219
  .filter((key) => value.includes(key))
140
- .forEach((val) =>
141
- bindings.set(val, [
142
- ...(bindings.get(val) || []),
143
- {
144
- el,
145
- prop: "hidden",
146
- expr: value,
147
- fn: compileExpression(
148
- `${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
149
- true,
150
- ),
151
- },
152
- ]),
153
- );
154
- 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(":")) {
155
236
  Object.keys(state)
156
237
  .filter((key) => value.includes(key))
157
- .forEach((val) =>
158
- bindings.set(val, [
159
- ...(bindings.get(val) || []),
160
- {
161
- el,
162
- prop: name.split(":")[1],
163
- expr: value,
164
- fn: compileExpression(value, true),
165
- },
166
- ]),
167
- );
168
- 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") {
169
251
  compileExpression(value, false)(
170
252
  $,
171
253
  state,
172
254
  undefined,
173
255
  el,
174
256
  html,
257
+ get,
258
+ post,
259
+ patch,
260
+ del,
175
261
  ...Object.values(data),
176
262
  ...[...refs.values()],
177
263
  );
178
- 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)
179
266
  const [eventName, ...modifiers] = name
180
267
  .slice(name.startsWith("@") ? 1 : 10)
181
268
  .split(".");
@@ -229,6 +316,10 @@ export default function helium(data = {}) {
229
316
  e,
230
317
  el,
231
318
  html,
319
+ get,
320
+ post,
321
+ patch,
322
+ del,
232
323
  ...Object.values(data),
233
324
  ...[...refs.values()],
234
325
  );
@@ -239,21 +330,33 @@ export default function helium(data = {}) {
239
330
 
240
331
  const debouncedHandler =
241
332
  debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
242
-
243
333
  receiver.addEventListener(eventName, debouncedHandler);
244
334
  }
245
- }
246
- });
247
- const observer = new MutationObserver((mutations) => {
248
- for (const m of mutations) {
249
- for (const node of m.addedNodes) {
250
- 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);
251
353
  }
252
354
  }
253
- });
254
- observer.observe(element, { childList: true, subtree: true });
255
- for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
256
- }
257
- 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);
258
361
  }
259
362
  helium();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "keywords": [