@daz4126/helium 0.11.1 → 0.13.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 +86 -50
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -3,68 +3,93 @@ 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] = [new Map(), new Map(), new WeakMap(), new WeakSet()];
9
10
  const $ = s => document.querySelector(s);
10
11
  const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
11
12
 
12
- const ajax=(u,m,o={},p={})=>{
13
- console.log("ajax: ", u,m,o.target,p)
14
- const fd=p instanceof FormData,t=document.querySelector('meta[name="csrf-token"]')?.content;
15
- fetch(u,{method:m,headers:{
16
- Accept:"text/vnd.turbo-stream.html,application/json,text/html",
17
- ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
18
- ...(t&&{"X-CSRF-Token":t})
19
- },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
20
- .then(r=>r.headers.get("content-type")?.includes("json")?r.json():r.text())
21
- .then(d=>{
22
- if(o.data)state[o.data]=d;
23
- if(o.target){
24
- const c=o.template?o.template(d):d;
25
- o.action?o.target[o.action=="replace"?"replaceWith":o.action](html(c)):o.target.innerHTML=c;
26
- }
27
- }).catch(e=>{
28
- console.error("AJAX:",e.message);
29
- })
30
- }
13
+ const update = (data,target,action,template) => {
14
+ const element = target instanceof Node ? target : (refs.get(target) || $(target));
15
+ if(element){
16
+ const content = html(template ? template(data) : data);
17
+ action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
18
+ return content
19
+ } else state[target] = data
20
+ }
21
+
22
+
23
+ const ajax = (u,m,o={},p={}) => {
24
+ if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
25
+ const fd = p instanceof FormData, t = document.querySelector('meta[name="csrf-token"]')?.content;
26
+ fetch(u,{method:m,headers:{
27
+ Accept:"text/vnd.turbo-stream.html,application/json,text/html",
28
+ ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
29
+ ...(t&&{"X-CSRF-Token":t})
30
+ },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
31
+ .then(r => {
32
+ const type = r.headers.get("content-type") || "";
33
+ return (type.includes("turbo-stream") ? r.text().then(d => ({ t: true, d })) :
34
+ type.includes("json") ? r.json() :
35
+ r.text());
36
+ }).then(d =>
37
+ d.t && "Turbo"
38
+ ? Turbo.renderStreamMessage(d.d)
39
+ : update(d, o.target, o.loading ? "replace" : o.action, o.template)
40
+ ).catch(e => console.error("AJAX:", e.message));
41
+ }
31
42
 
32
43
  const get = (u,t) => ajax(u,"GET",t);
33
44
  const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
34
45
 
35
46
  const handler = {
36
- get: (t, p, r) => {const v = Reflect.get(t, p, r); return typeof v == "object" && v != null ? new Proxy(v, handler) : v},
37
- set: (t, p, v) => {const res = Reflect.set(t, p, v); bindings.get(p)?.forEach(applyBinding); return res}
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}
38
49
  };
39
50
 
40
51
  const state = new Proxy(data, handler);
41
52
  let isUpdatingDOM = false;
42
53
 
43
54
  function applyBinding(b,e={},elCtx=b.el){
44
- const {el,prop,fn}=b;
45
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
46
- if(prop=="innerHTML"){isUpdatingDOM=1;el.innerHTML=Array.isArray(r)?r.join``:r;isUpdatingDOM=0}
47
- 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));
48
- else if(prop=="style"&&r&&typeof r=="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
49
- else if(prop in el){if(el.type=="radio")el.checked=el.value==r;else el[prop]=prop=="textContent"?r:parseEx(r)}
50
- else el.setAttribute(prop,parseEx(r))
51
- }
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))
67
+ }
52
68
 
53
69
  const compile = (expr, withReturn = false) => {
54
70
  try {
55
- return new Function("$", "$data", "$event", "$el", "$html","$get", "$post", "$put","$patch","$delete",...Object.keys(data), ...[...refs.keys()],
56
- `with($data){${withReturn ? "return" : ""}(${expr.trim()})}`);
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()})}`);
57
73
  } catch {return () => expr}
58
74
  };
59
75
 
60
- const trackDependencies=fn=>{
61
- 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}});
62
- try{fn($,p,refs)}catch{};return[...s]
63
- }
76
+ const trackDependencies = (fn, el) => {
77
+ const accessed = new Set();
78
+ const trackProxy = new Proxy(data, {
79
+ get(target, prop) {
80
+ if (typeof prop == 'string') accessed.add(prop);
81
+ const val = target[prop];
82
+ return typeof val == "object" && val != null ? new Proxy(val, this) : val;
83
+ }
84
+ });
85
+
86
+ try { fn.call(null, $, trackProxy, refs); } catch {}
87
+ return [...accessed];
88
+ };
64
89
 
65
90
  const cleanup = el => {
66
- [el, ...el.querySelectorAll('*')].forEach(e => {
67
- listeners.get(e)?.forEach(({receiver, event, handler}) => receiver.removeEventListener(event, handler));
91
+ [el,...el.querySelectorAll('*')].forEach(e => {
92
+ listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
68
93
  listeners.delete(e);
69
94
  });
70
95
  };
@@ -72,8 +97,8 @@ const trackDependencies=fn=>{
72
97
  function processElements(element) {
73
98
  const newBindings = [];
74
99
  if (element.nodeType == 1) {
75
- if (element.hasAttribute?.("data-he-p")) return newBindings;
76
- element.setAttribute("data-he-p", "");
100
+ if (processed.has(element)) return newBindings;
101
+ processed.add(element);
77
102
  }
78
103
 
79
104
  const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
@@ -95,6 +120,9 @@ const trackDependencies=fn=>{
95
120
  };
96
121
 
97
122
  heElements.forEach(el => {
123
+ if (processed.has(el)) return;
124
+ processed.add(el);
125
+
98
126
  const execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
99
127
  const inputType = el.type?.toLowerCase();
100
128
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
@@ -128,7 +156,7 @@ const trackDependencies=fn=>{
128
156
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
129
157
  }
130
158
  if (he(name,"init")) {
131
- execFn(value);
159
+ initFn = compile(value,true);
132
160
  } else if (name.startsWith("@") || name.startsWith("data-he")) {
133
161
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
134
162
  const [eventName, ...mods] = fullName.split(".");
@@ -151,14 +179,21 @@ const trackDependencies=fn=>{
151
179
  }
152
180
  if (!mods.includes("outside") || !el.contains(e.target)) {
153
181
  if (isHttpMethod) {
154
- const options = exFn(el.getAttribute('data-he-options') || el.getAttribute('options') || '{}');
155
- let paramsAttr = el.getAttribute('data-he-params') || el.getAttribute('params') || '{}';
182
+ const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
183
+ const [target, action] = (getAttr('target') || "").split(":");
184
+ const options = {
185
+ ...exFn(getAttr('options') || '{}'),
186
+ ...(target && { target }),
187
+ ...(action && { action }),
188
+ ...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
189
+ ...getAttr('loading') && { loading: getAttr('loading') }
190
+ };
191
+ let paramsAttr = getAttr('params') || '{}';
156
192
  if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
157
193
  const props = paramsAttr.split(":").map(s => s.trim());
158
194
  paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
159
195
  }
160
196
  const params = exFn(paramsAttr);
161
- console.log("params: ",params)
162
197
  ajax(value, eventName.toUpperCase(), options, params);
163
198
  } else {
164
199
  exFn(value);
@@ -176,12 +211,13 @@ const trackDependencies=fn=>{
176
211
  return newBindings;
177
212
  }
178
213
  new MutationObserver(ms=>{
179
- if(isUpdatingDOM)return;
180
- for(const m of ms){
181
- m.removedNodes.forEach(n=>n.nodeType==1&&cleanup(n));
182
- m.addedNodes.forEach(n=>n.nodeType==1&&!n.hasAttribute("data-he-p")&&processElements(n).forEach(applyBinding));
183
- }
184
- }).observe(root,{childList:1,subtree:1});
214
+ if(isUpdatingDOM)return;
215
+ for(const m of ms){
216
+ m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
217
+ m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
218
+ }
219
+ }).observe(root,{childList:1,subtree:1});
185
220
  processElements(root);
186
221
  for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
222
+ if(initFn) initFn($, state, {}, {}, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()])
187
223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {