@daz4126/helium 0.14.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 +119 -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, parentKeys] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap()];
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
 
@@ -43,15 +43,17 @@ const ajax = (u,m,o={},p={}) => {
43
43
  const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
44
44
 
45
45
  const handler = {
46
- get: (t,p,r) => {
47
- const v = Reflect.get(t,p,r);
48
- if (typeof v==="object" && v!==null) {
49
- const proxy = new Proxy(v, handler);
50
- parentKeys.set(v, p);
51
- return proxy;
52
- }
53
- return v;
54
- },
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
+ },
55
57
  set: (t,p,v) => {
56
58
  const res = Reflect.set(t,p,v);
57
59
  if (Array.isArray(t) && !isNaN(p)) {
@@ -64,11 +66,40 @@ const handler = {
64
66
  };
65
67
 
66
68
  const state = new Proxy(data, handler);
67
-
69
+
68
70
  function applyBinding(b,e={},elCtx=b.el){
69
- const {el,prop,fn}=b;
70
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
71
- if(prop==="innerHTML" && Array.isArray(r) && el.children.length > 0){
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;
82
+ }
83
+
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){
72
103
  const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
73
104
  const newChildren = [...temp.children];
74
105
 
@@ -104,22 +135,23 @@ function applyBinding(b,e={},elCtx=b.el){
104
135
  }
105
136
  }
106
137
  }
107
- } else if(prop==="innerHTML"){
108
- const content = Array.isArray(r)?r.join``:r
109
- typeof Idiomorph === "object" ? Idiomorph.morph(el, content,{morphStyle:'innerHTML'}) : el.innerHTML = content;
110
- }
111
- 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));
112
- else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
113
- else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
114
- else el.setAttribute(prop,parseEx(r))
115
- }
138
+ }
116
139
 
117
- const compile = (expr, withReturn = false) => {
118
- try {
119
- return new Function("$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",...Object.keys(data),...[...refs.keys()],
120
- `with($data){${withReturn?"return":""}(${expr.trim()})}`);
121
- } catch {return () => expr}
122
- };
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
+ };
123
155
 
124
156
  const trackDependencies = (fn, el) => {
125
157
  const accessed = new Set();
@@ -142,24 +174,11 @@ const trackDependencies = (fn, el) => {
142
174
  });
143
175
  };
144
176
 
145
- function processElements(element) {
146
- let count = 0
177
+ function processElements(element) {
147
178
  const newBindings = [];
148
- if (element.nodeType == 1 && processed.has(element)) return newBindings;
149
179
 
150
- const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
151
-
152
- heElements.forEach(el => {
153
- count++
154
- for (const {name, value} of el.attributes || []) {
155
- if (he(name,"text","html","bind")) {
156
- try {
157
- new Function(`let ${value}=1`);
158
- state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
159
- } catch {}
160
- }
161
- }
162
- });
180
+ const heElements = [element, ...element.querySelectorAll("*")]
181
+ .filter(e => !processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
163
182
 
164
183
  const addBinding = (val, b) => {
165
184
  bindings.set(val, [...(bindings.get(val) || []), b]);
@@ -167,75 +186,99 @@ const trackDependencies = (fn, el) => {
167
186
  };
168
187
 
169
188
  heElements.forEach(el => {
170
- if (processed.has(el)) return;
171
189
  processed.add(el);
172
190
 
173
- 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()]);
174
193
  const inputType = el.type?.toLowerCase();
175
194
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
176
195
 
177
- for (const {name, value} of el.attributes || []) {
178
- if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
179
- if (he(name,"ref")) refs.set("$" + value, el);
180
- 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")) {
181
219
  const fn = compile(value, true);
182
- const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
220
+ const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
183
221
  trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
184
222
  }
185
- if (he(name,"bind")) {
223
+ else if (he(name, "bind")) {
186
224
  const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
187
225
  const prop = isCheckbox ? "checked" : "value";
188
- 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;
189
227
  el.addEventListener(event, inputHandler);
190
228
  if (!listeners.has(el)) listeners.set(el, []);
191
229
  listeners.get(el).push({receiver: el, event, handler: inputHandler});
192
- addBinding(value, {el, prop, fn: compile(value, true)});
230
+ addBinding(value, {el, prop, fn: compile(value, true)});
193
231
  if (isCheckbox) el.checked = !!state[value];
194
232
  else if (isRadio) el.checked = el.value == state[value];
195
233
  else el.value = state[value] ?? "";
196
234
  }
197
- if (he(name,"hidden","visible")) {
198
- const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
235
+ else if (he(name, "hidden", "visible")) {
236
+ const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
199
237
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
200
238
  }
201
- 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(":")) {
202
244
  const fn = compile(value, true);
203
245
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
204
246
  }
205
- if (he(name,"init")) {
206
- initFn = compile(value,true);
207
- } 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")) {
208
251
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
209
252
  const [eventName, ...mods] = fullName.split(".");
210
- const isHttpMethod = ["get","post","put","patch","delete"].includes(eventName);
253
+ const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
211
254
  const event = isHttpMethod ? getEvent(el) : eventName;
212
- const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
255
+ const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
213
256
  const debounceMod = mods.find(m => m.startsWith("debounce"));
214
257
  const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
215
258
  const _handler = e => {
216
- const exFn = v => compile(v, true)($, state, e, el, html,get,post,put,patch,del, ...Object.values(data), ...[...refs.values()])
217
- 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();
218
261
  const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
219
- for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
220
- 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)) {
221
264
  const last = mods[mods.length - 1];
222
- if (last && !["prevent","once","outside","document"].includes(last)) {
265
+ if (last && !["prevent", "once", "outside", "document"].includes(last)) {
223
266
  const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
224
267
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
225
268
  }
226
- }
227
- if (!mods.includes("outside") || !el.contains(e.target)) {
269
+ }
270
+ if (!mods.includes("outside") || !el.contains(e.target)) {
228
271
  if (isHttpMethod) {
229
272
  const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
230
273
  const [target, action] = (getAttr('target') || "").split(":");
231
274
  const options = {
232
275
  ...exFn(getAttr('options') || '{}'),
233
- ...(target && { target }),
234
- ...(action && { action }),
235
- ...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
236
- ...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')}
237
280
  };
238
- let paramsAttr = getAttr('params') || '{}';
281
+ let paramsAttr = getAttr('params') || '{}';
239
282
  if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
240
283
  const props = paramsAttr.split(":").map(s => s.trim());
241
284
  paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
@@ -245,7 +288,7 @@ const trackDependencies = (fn, el) => {
245
288
  } else {
246
289
  exFn(value);
247
290
  }
248
- }
291
+ }
249
292
  if (mods.includes("once")) receiver.removeEventListener(event, handler);
250
293
  };
251
294
  const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {