@daz4126/helium 0.12.0 → 0.14.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 +95 -23
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -3,9 +3,10 @@ const getEvent = el => ({form:"submit",input:"input",textarea:"input",select:"ch
3
3
  const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
4
4
 
5
5
  export default function helium(data = {}) {
6
+ let initFn;
6
7
  const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
7
8
  const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
8
- const [bindings, refs, listeners] = [new Map(), new Map(), new WeakMap()];
9
+ const [bindings, refs, listeners, processed, parentKeys] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap()];
9
10
  const $ = s => document.querySelector(s);
10
11
  const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
11
12
 
@@ -16,8 +17,7 @@ export default function helium(data = {}) {
16
17
  action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
17
18
  return content
18
19
  } else state[target] = data
19
- }
20
-
20
+ }
21
21
 
22
22
  const ajax = (u,m,o={},p={}) => {
23
23
  if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
@@ -27,26 +27,87 @@ const ajax = (u,m,o={},p={}) => {
27
27
  ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
28
28
  ...(t&&{"X-CSRF-Token":t})
29
29
  },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
30
- .then(r=>r.headers.get("content-type")?.includes("json")?r.json():r.text())
31
- .then(d=>o.loading?update(d,o.target,"replace",o.template):update(d,o.target,o.action,o.template))
32
- .catch(e=>console.error("AJAX:",e.message));
30
+ .then(r => {
31
+ const type = r.headers.get("content-type") || "";
32
+ return (type.includes("turbo-stream") ? r.text().then(d => ({ t: true, d })) :
33
+ type.includes("json") ? r.json() :
34
+ r.text());
35
+ }).then(d =>
36
+ d.t && "Turbo"
37
+ ? Turbo.renderStreamMessage(d.d)
38
+ : update(d, o.target, o.loading ? "replace" : o.action, o.template)
39
+ ).catch(e => console.error("AJAX:", e.message));
33
40
  }
34
41
 
35
42
  const get = (u,t) => ajax(u,"GET",t);
36
43
  const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
37
44
 
38
- const handler = {
39
- get: (t,p,r) => {const v = Reflect.get(t,p,r); return typeof v==="object"&&v!==null ? new Proxy(v,handler) : v},
40
- set: (t,p,v) => {const res = Reflect.set(t,p,v); bindings.get(p)?.forEach(applyBinding); return res}
41
- };
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
+ },
55
+ set: (t,p,v) => {
56
+ const res = Reflect.set(t,p,v);
57
+ if (Array.isArray(t) && !isNaN(p)) {
58
+ const parentKey = parentKeys.get(t);
59
+ if (parentKey) bindings.get(parentKey)?.forEach(applyBinding);
60
+ }
61
+ bindings.get(p)?.forEach(applyBinding);
62
+ return res
63
+ }
64
+ };
42
65
 
43
66
  const state = new Proxy(data, handler);
44
- let isUpdatingDOM = false;
45
67
 
46
68
  function applyBinding(b,e={},elCtx=b.el){
47
69
  const {el,prop,fn}=b;
48
70
  const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
49
- if(prop==="innerHTML"){isUpdatingDOM=1;el.innerHTML=Array.isArray(r)?r.join``:r;isUpdatingDOM=0}
71
+ if(prop==="innerHTML" && Array.isArray(r) && el.children.length > 0){
72
+ const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
73
+ const newChildren = [...temp.children];
74
+
75
+ const newItems = newChildren.map((child, idx) => ({
76
+ key: child.getAttribute("key") || child.dataset.key,
77
+ element: child,
78
+ index: idx
79
+ }));
80
+
81
+ const existingItems = [...el.children].map((child, idx) => ({
82
+ key: child.getAttribute("key") || child.dataset.key,
83
+ element: child,
84
+ index: idx
85
+ }));
86
+
87
+ for (let i = 0; i < Math.max(newItems.length, existingItems.length); i++) {
88
+ const existing = existingItems[i];
89
+ const newItem = newItems[i];
90
+
91
+ if (!newItem && existing) {
92
+ cleanup(existing.element);
93
+ existing.element.remove();
94
+ } else if (newItem && !existing) {
95
+ el.appendChild(newItem.element);
96
+ } else if (newItem && existing) {
97
+ if (existing.element.outerHTML !== newItem.element.outerHTML) {
98
+ if(typeof Idiomorph === "object") {
99
+ Idiomorph.morph(existing.element, newItem.element.outerHTML);
100
+ } else {
101
+ cleanup(existing.element);
102
+ existing.element.replaceWith(newItem.element);
103
+ }
104
+ }
105
+ }
106
+ }
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
+ }
50
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));
51
112
  else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
52
113
  else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
@@ -60,10 +121,19 @@ function applyBinding(b,e={},elCtx=b.el){
60
121
  } catch {return () => expr}
61
122
  };
62
123
 
63
- const trackDependencies = fn => {
64
- const s=new Set(),p=new Proxy(state,{get:(t,k)=>{if(typeof k==="string")s.add(k);const v=t[k];return v&&typeof v==="object"?new Proxy(v,this):v}});
65
- try{fn($,p,refs)}catch{};return[...s]
66
- }
124
+ const trackDependencies = (fn, el) => {
125
+ const accessed = new Set();
126
+ const trackProxy = new Proxy(data, {
127
+ get(target, prop) {
128
+ if (typeof prop == 'string') accessed.add(prop);
129
+ const val = target[prop];
130
+ return typeof val == "object" && val != null ? new Proxy(val, this) : val;
131
+ }
132
+ });
133
+
134
+ try { fn.call(null, $, trackProxy, refs); } catch {}
135
+ return [...accessed];
136
+ };
67
137
 
68
138
  const cleanup = el => {
69
139
  [el,...el.querySelectorAll('*')].forEach(e => {
@@ -73,15 +143,14 @@ const trackDependencies = fn => {
73
143
  };
74
144
 
75
145
  function processElements(element) {
146
+ let count = 0
76
147
  const newBindings = [];
77
- if (element.nodeType == 1) {
78
- if (element.hasAttribute?.("data-he-p")) return newBindings;
79
- element.setAttribute("data-he-p", "");
80
- }
148
+ if (element.nodeType == 1 && processed.has(element)) return newBindings;
81
149
 
82
150
  const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
83
151
 
84
152
  heElements.forEach(el => {
153
+ count++
85
154
  for (const {name, value} of el.attributes || []) {
86
155
  if (he(name,"text","html","bind")) {
87
156
  try {
@@ -98,6 +167,9 @@ const trackDependencies = fn => {
98
167
  };
99
168
 
100
169
  heElements.forEach(el => {
170
+ if (processed.has(el)) return;
171
+ processed.add(el);
172
+
101
173
  const execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
102
174
  const inputType = el.type?.toLowerCase();
103
175
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
@@ -131,7 +203,7 @@ const trackDependencies = fn => {
131
203
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
132
204
  }
133
205
  if (he(name,"init")) {
134
- execFn(value);
206
+ initFn = compile(value,true);
135
207
  } else if (name.startsWith("@") || name.startsWith("data-he")) {
136
208
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
137
209
  const [eventName, ...mods] = fullName.split(".");
@@ -186,12 +258,12 @@ const trackDependencies = fn => {
186
258
  return newBindings;
187
259
  }
188
260
  new MutationObserver(ms=>{
189
- if(isUpdatingDOM)return;
190
261
  for(const m of ms){
191
262
  m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
192
- m.addedNodes.forEach(n=>n.nodeType===1&&!n.hasAttribute("data-he-p")&&processElements(n).forEach(applyBinding));
263
+ m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
193
264
  }
194
265
  }).observe(root,{childList:1,subtree:1});
195
266
  processElements(root);
196
267
  for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
268
+ if(initFn) initFn($, state, {}, {}, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()])
197
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {