@daz4126/helium 0.13.0 → 0.15.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 (2) hide show
  1. package/helium.js +165 -76
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -6,7 +6,7 @@ export default function helium(data = {}) {
6
6
  let initFn;
7
7
  const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
8
8
  const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
9
- const [bindings, refs, listeners, processed] = [new Map(), new Map(), new WeakMap(), new WeakSet()];
9
+ const [bindings, refs, listeners, processed, parentKeys, fnCache, proxyCache] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap(), new Map(), new WeakMap()];
10
10
  const $ = s => document.querySelector(s);
11
11
  const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
12
12
 
@@ -17,8 +17,7 @@ export default function helium(data = {}) {
17
17
  action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
18
18
  return content
19
19
  } else state[target] = data
20
- }
21
-
20
+ }
22
21
 
23
22
  const ajax = (u,m,o={},p={}) => {
24
23
  if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
@@ -43,35 +42,116 @@ const ajax = (u,m,o={},p={}) => {
43
42
  const get = (u,t) => ajax(u,"GET",t);
44
43
  const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
45
44
 
46
- const handler = {
47
- get: (t,p,r) => {const v = Reflect.get(t,p,r); return typeof v==="object"&&v!==null ? new Proxy(v,handler) : v},
48
- set: (t,p,v) => {const res = Reflect.set(t,p,v); bindings.get(p)?.forEach(applyBinding); return res}
49
- };
45
+ const handler = {
46
+ get(t,p,r) {
47
+ const v = Reflect.get(t,p,r);
48
+ if (v && typeof v === "object") {
49
+ if (proxyCache.has(v)) return proxyCache.get(v);
50
+ const proxy = new Proxy(v, handler);
51
+ proxyCache.set(v, proxy);
52
+ parentKeys.set(v, p);
53
+ return proxy;
54
+ }
55
+ return v;
56
+ },
57
+ set: (t,p,v) => {
58
+ const res = Reflect.set(t,p,v);
59
+ if (Array.isArray(t) && !isNaN(p)) {
60
+ const parentKey = parentKeys.get(t);
61
+ if (parentKey) bindings.get(parentKey)?.forEach(applyBinding);
62
+ }
63
+ bindings.get(p)?.forEach(applyBinding);
64
+ return res
65
+ }
66
+ };
50
67
 
51
68
  const state = new Proxy(data, handler);
52
- let isUpdatingDOM = false;
53
-
69
+
54
70
  function applyBinding(b,e={},elCtx=b.el){
55
- const {el,prop,fn}=b;
56
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
57
- if(prop==="innerHTML"){
58
- isUpdatingDOM=1;
59
- const content = el.innerHTML=Array.isArray(r)?r.join``:r
60
- typeof Idiomorph === "object" ? Idiomorph.morph(el, content,{morphStyle:'innerHTML'}) : el.innerHTML = content;
61
- isUpdatingDOM=0
62
- }
63
- else if(prop==="class"&&r&&typeof r==="object")for(const[k,v]of Object.entries(r))k.split(/\s+/).forEach(c=>el.classList.toggle(c,v));
64
- else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
65
- else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
66
- else el.setAttribute(prop,parseEx(r))
71
+ const {el,prop,fn}=b;
72
+ const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
73
+
74
+ if (prop==="innerHTML" && Array.isArray(r) && el.children.length > 0)
75
+ return updateList(el, r);
76
+
77
+ if (prop==="innerHTML") {
78
+ const content = Array.isArray(r)?r.join``:r;
79
+ return typeof Idiomorph === "object"
80
+ ? Idiomorph.morph(el, content,{morphStyle:'innerHTML'})
81
+ : el.innerHTML = content;
67
82
  }
68
83
 
69
- const compile = (expr, withReturn = false) => {
70
- try {
71
- return new Function("$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",...Object.keys(data),...[...refs.keys()],
72
- `with($data){${withReturn?"return":""}(${expr.trim()})}`);
73
- } catch {return () => expr}
74
- };
84
+ if (prop==="class" && r && typeof r==="object")
85
+ return Object.entries(r).forEach(([k,v]) =>
86
+ k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
87
+
88
+ if (prop==="style" && r && typeof r==="object")
89
+ return el.style = Object.entries(r).filter(([,v])=>v)
90
+ .map(([k,v])=>`${k}:${v}`).join(";");
91
+
92
+ if (prop in el) {
93
+ if(el.type==="radio") el.checked = el.value===r;
94
+ else el[prop] = prop==="textContent"?r:parseEx(r);
95
+ return;
96
+ }
97
+
98
+ el.setAttribute(prop, parseEx(r));
99
+ }
100
+
101
+
102
+ function updateList(el,r){
103
+ const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
104
+ const newChildren = [...temp.children];
105
+
106
+ const newItems = newChildren.map((child, idx) => ({
107
+ key: child.getAttribute("key") || child.dataset.key,
108
+ element: child,
109
+ index: idx
110
+ }));
111
+
112
+ const existingItems = [...el.children].map((child, idx) => ({
113
+ key: child.getAttribute("key") || child.dataset.key,
114
+ element: child,
115
+ index: idx
116
+ }));
117
+
118
+ for (let i = 0; i < Math.max(newItems.length, existingItems.length); i++) {
119
+ const existing = existingItems[i];
120
+ const newItem = newItems[i];
121
+
122
+ if (!newItem && existing) {
123
+ cleanup(existing.element);
124
+ existing.element.remove();
125
+ } else if (newItem && !existing) {
126
+ el.appendChild(newItem.element);
127
+ } else if (newItem && existing) {
128
+ if (existing.element.outerHTML !== newItem.element.outerHTML) {
129
+ if(typeof Idiomorph === "object") {
130
+ Idiomorph.morph(existing.element, newItem.element.outerHTML);
131
+ } else {
132
+ cleanup(existing.element);
133
+ existing.element.replaceWith(newItem.element);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ const compile = (expr, withReturn = false) => {
141
+ const key = `${withReturn}:${expr}`;
142
+ if (fnCache.has(key)) return fnCache.get(key);
143
+ try {
144
+ const fn = new Function(
145
+ "$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
146
+ ...Object.keys(data), ...[...refs.keys()],
147
+ `with($data){${withReturn?"return":""}(${expr.trim()})}`
148
+ );
149
+ fnCache.set(key, fn);
150
+ return fn;
151
+ } catch {
152
+ return () => expr;
153
+ }
154
+ };
75
155
 
76
156
  const trackDependencies = (fn, el) => {
77
157
  const accessed = new Set();
@@ -94,25 +174,11 @@ const trackDependencies = (fn, el) => {
94
174
  });
95
175
  };
96
176
 
97
- function processElements(element) {
177
+ function processElements(element) {
98
178
  const newBindings = [];
99
- if (element.nodeType == 1) {
100
- if (processed.has(element)) return newBindings;
101
- processed.add(element);
102
- }
103
-
104
- const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
105
179
 
106
- heElements.forEach(el => {
107
- for (const {name, value} of el.attributes || []) {
108
- if (he(name,"text","html","bind")) {
109
- try {
110
- new Function(`let ${value}=1`);
111
- state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
112
- } catch {}
113
- }
114
- }
115
- });
180
+ const heElements = [element, ...element.querySelectorAll("*")]
181
+ .filter(e => !processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
116
182
 
117
183
  const addBinding = (val, b) => {
118
184
  bindings.set(val, [...(bindings.get(val) || []), b]);
@@ -120,75 +186,99 @@ const trackDependencies = (fn, el) => {
120
186
  };
121
187
 
122
188
  heElements.forEach(el => {
123
- if (processed.has(el)) return;
124
189
  processed.add(el);
125
190
 
126
- const execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
191
+ const attrs = el.attributes;
192
+ const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
127
193
  const inputType = el.type?.toLowerCase();
128
194
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
129
195
 
130
- for (const {name, value} of el.attributes || []) {
131
- if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
132
- if (he(name,"ref")) refs.set("$" + value, el);
133
- if (he(name,"text","html")) {
196
+ // Single loop through attributes with early skipping
197
+ for (let i = 0; i < attrs.length; i++) {
198
+ const {name, value} = attrs[i];
199
+
200
+ // Skip non-helium attributes early
201
+ if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
202
+
203
+ // Initialize state first if needed
204
+ if (he(name, "text", "html", "bind")) {
205
+ try {
206
+ new Function(`let ${value}=1`);
207
+ state[value] ??= he(name, "bind") ? (el.type == "checkbox" ? el.checked : el.value) : el.textContent;
208
+ } catch {}
209
+ }
210
+
211
+ // Process the attribute
212
+ if (["@data", "data-he"].includes(name)) {
213
+ Object.assign(state, execFn(value));
214
+ }
215
+ else if (he(name, "ref")) {
216
+ refs.set("$" + value, el);
217
+ }
218
+ else if (he(name, "text", "html")) {
134
219
  const fn = compile(value, true);
135
- const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
220
+ const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
136
221
  trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
137
222
  }
138
- if (he(name,"bind")) {
223
+ else if (he(name, "bind")) {
139
224
  const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
140
225
  const prop = isCheckbox ? "checked" : "value";
141
- const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
226
+ const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
142
227
  el.addEventListener(event, inputHandler);
143
228
  if (!listeners.has(el)) listeners.set(el, []);
144
229
  listeners.get(el).push({receiver: el, event, handler: inputHandler});
145
- addBinding(value, {el, prop, fn: compile(value, true)});
230
+ addBinding(value, {el, prop, fn: compile(value, true)});
146
231
  if (isCheckbox) el.checked = !!state[value];
147
232
  else if (isRadio) el.checked = el.value == state[value];
148
233
  else el.value = state[value] ?? "";
149
234
  }
150
- if (he(name,"hidden","visible")) {
151
- const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
235
+ else if (he(name, "hidden", "visible")) {
236
+ const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
152
237
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
153
238
  }
154
- if (name.startsWith(":")) {
239
+ else if (he(name, "class")) {
240
+ const fn = compile(value, true);
241
+ trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn}));
242
+ }
243
+ else if (name.startsWith(":")) {
155
244
  const fn = compile(value, true);
156
245
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
157
246
  }
158
- if (he(name,"init")) {
159
- initFn = compile(value,true);
160
- } else if (name.startsWith("@") || name.startsWith("data-he")) {
247
+ else if (he(name, "init")) {
248
+ initFn = compile(value, true);
249
+ }
250
+ else if (name.startsWith("@") || name.startsWith("data-he")) {
161
251
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
162
252
  const [eventName, ...mods] = fullName.split(".");
163
- const isHttpMethod = ["get","post","put","patch","delete"].includes(eventName);
253
+ const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
164
254
  const event = isHttpMethod ? getEvent(el) : eventName;
165
- const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
255
+ const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
166
256
  const debounceMod = mods.find(m => m.startsWith("debounce"));
167
257
  const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
168
258
  const _handler = e => {
169
- const exFn = v => compile(v, true)($, state, e, el, html,get,post,put,patch,del, ...Object.values(data), ...[...refs.values()])
170
- if (mods.includes("prevent")) e.preventDefault();
259
+ const exFn = v => compile(v, true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()])
260
+ if (mods.includes("prevent")) e.preventDefault();
171
261
  const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
172
- for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
173
- if (["keydown","keyup","keypress"].includes(event)) {
262
+ for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
263
+ if (["keydown", "keyup", "keypress"].includes(event)) {
174
264
  const last = mods[mods.length - 1];
175
- if (last && !["prevent","once","outside","document"].includes(last)) {
265
+ if (last && !["prevent", "once", "outside", "document"].includes(last)) {
176
266
  const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
177
267
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
178
268
  }
179
- }
180
- if (!mods.includes("outside") || !el.contains(e.target)) {
269
+ }
270
+ if (!mods.includes("outside") || !el.contains(e.target)) {
181
271
  if (isHttpMethod) {
182
272
  const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
183
273
  const [target, action] = (getAttr('target') || "").split(":");
184
274
  const options = {
185
275
  ...exFn(getAttr('options') || '{}'),
186
- ...(target && { target }),
187
- ...(action && { action }),
188
- ...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
189
- ...getAttr('loading') && { loading: getAttr('loading') }
276
+ ...(target && {target}),
277
+ ...(action && {action}),
278
+ ...execFn(getAttr('template')) && {template: execFn(getAttr('template'))},
279
+ ...getAttr('loading') && {loading: getAttr('loading')}
190
280
  };
191
- let paramsAttr = getAttr('params') || '{}';
281
+ let paramsAttr = getAttr('params') || '{}';
192
282
  if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
193
283
  const props = paramsAttr.split(":").map(s => s.trim());
194
284
  paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
@@ -198,7 +288,7 @@ const trackDependencies = (fn, el) => {
198
288
  } else {
199
289
  exFn(value);
200
290
  }
201
- }
291
+ }
202
292
  if (mods.includes("once")) receiver.removeEventListener(event, handler);
203
293
  };
204
294
  const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
@@ -211,7 +301,6 @@ const trackDependencies = (fn, el) => {
211
301
  return newBindings;
212
302
  }
213
303
  new MutationObserver(ms=>{
214
- if(isUpdatingDOM)return;
215
304
  for(const m of ms){
216
305
  m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
217
306
  m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {