@daz4126/helium 0.11.0 → 0.12.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 +73 -72
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -7,58 +7,67 @@ export default function helium(data = {}) {
7
7
  const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
8
8
  const [bindings, refs, listeners] = [new Map(), new Map(), new WeakMap()];
9
9
  const $ = s => document.querySelector(s);
10
- const html = s => {const t = document.createElement("template"); t.innerHTML = s.trim(); return t.content.firstChild};
10
+ const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
11
+
12
+ const update = (data,target,action,template) => {
13
+ const element = target instanceof Node ? target : (refs.get(target) || $(target));
14
+ if(element){
15
+ const content = html(template ? template(data) : data);
16
+ action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
17
+ return content
18
+ } else state[target] = data
19
+ }
20
+
21
+
22
+ const ajax = (u,m,o={},p={}) => {
23
+ if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
24
+ const fd = p instanceof FormData, t = document.querySelector('meta[name="csrf-token"]')?.content;
25
+ fetch(u,{method:m,headers:{
26
+ Accept:"text/vnd.turbo-stream.html,application/json,text/html",
27
+ ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
28
+ ...(t&&{"X-CSRF-Token":t})
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));
33
+ }
11
34
 
12
- const ajax=(u,m,o={},p={})=>{
13
- const fd=p instanceof FormData,t=document.querySelector('meta[name="csrf-token"]')?.content;
14
- fetch(u,{method:m,headers:{
15
- Accept:"text/vnd.turbo-stream.html,application/json,text/html",
16
- ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
17
- ...(t&&{"X-CSRF-Token":t})
18
- },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
19
- .then(r=>r.headers.get("content-type")?.includes("json")?r.json():r.text())
20
- .then(d=>{
21
- if(o.data)state[o.data]=d;
22
- if(o.target){
23
- const c=o.template?o.template(d):d;
24
- o.action?o.target[o.action=="replace"?"replaceWith":o.action](html(c)):o.target.innerHTML=c;
25
- }
26
- }).catch(e=>console.error("AJAX:",e.message))
27
- }
35
+ const get = (u,t) => ajax(u,"GET",t);
36
+ const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
28
37
 
29
38
  const handler = {
30
- get: (t, p, r) => {const v = Reflect.get(t, p, r); return typeof v == "object" && v != null ? new Proxy(v, handler) : v},
31
- set: (t, p, v) => {const res = Reflect.set(t, p, v); bindings.get(p)?.forEach(applyBinding); return res}
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}
32
41
  };
33
42
 
34
43
  const state = new Proxy(data, handler);
35
44
  let isUpdatingDOM = false;
36
45
 
37
46
  function applyBinding(b,e={},elCtx=b.el){
38
- const {el,prop,fn}=b;
39
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
40
- if(prop=="innerHTML"){isUpdatingDOM=1;el.innerHTML=Array.isArray(r)?r.join``:r;isUpdatingDOM=0}
41
- 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));
42
- else if(prop=="style"&&r&&typeof r=="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
43
- else if(prop in el){if(el.type=="radio")el.checked=el.value==r;else el[prop]=prop=="textContent"?r:parseEx(r)}
44
- else el.setAttribute(prop,parseEx(r))
45
- }
47
+ const {el,prop,fn}=b;
48
+ 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}
50
+ 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
+ else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
52
+ else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
53
+ else el.setAttribute(prop,parseEx(r))
54
+ }
46
55
 
47
56
  const compile = (expr, withReturn = false) => {
48
57
  try {
49
- return new Function("$", "$data", "$event", "$el", "$html", ...Object.keys(data), ...[...refs.keys()],
50
- `with($data){${withReturn ? "return" : ""}(${expr.trim()})}`);
58
+ return new Function("$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",...Object.keys(data),...[...refs.keys()],
59
+ `with($data){${withReturn?"return":""}(${expr.trim()})}`);
51
60
  } catch {return () => expr}
52
61
  };
53
62
 
54
- const trackDependencies=fn=>{
55
- const s=new Set(),p=new Proxy(data,{get:(t,k)=>{if(typeof k=="string")s.add(k);const v=t[k];return v&&typeof v=="object"?new Proxy(v,this):v}});
56
- try{fn($,p,refs)}catch{};return[...s]
57
- }
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
+ }
58
67
 
59
68
  const cleanup = el => {
60
- [el, ...el.querySelectorAll('*')].forEach(e => {
61
- listeners.get(e)?.forEach(({receiver, event, handler}) => receiver.removeEventListener(event, handler));
69
+ [el,...el.querySelectorAll('*')].forEach(e => {
70
+ listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
62
71
  listeners.delete(e);
63
72
  });
64
73
  };
@@ -89,47 +98,40 @@ const trackDependencies=fn=>{
89
98
  };
90
99
 
91
100
  heElements.forEach(el => {
92
- const execFn = v => compile(v, true)($, state, {}, el, html, ...Object.values(data), ...[...refs.values()]);
101
+ const execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
93
102
  const inputType = el.type?.toLowerCase();
94
103
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
95
104
 
96
105
  for (const {name, value} of el.attributes || []) {
97
106
  if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
98
- if (he(name,"ref")) refs.set("$" + value, el);
99
-
107
+ if (he(name,"ref")) refs.set("$" + value, el);
100
108
  if (he(name,"text","html")) {
101
109
  const fn = compile(value, true);
102
110
  const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
103
111
  trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
104
112
  }
105
-
106
113
  if (he(name,"bind")) {
107
114
  const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
108
115
  const prop = isCheckbox ? "checked" : "value";
109
- const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
110
-
116
+ const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
111
117
  el.addEventListener(event, inputHandler);
112
118
  if (!listeners.has(el)) listeners.set(el, []);
113
119
  listeners.get(el).push({receiver: el, event, handler: inputHandler});
114
- addBinding(value, {el, prop, fn: compile(value, true)});
115
-
120
+ addBinding(value, {el, prop, fn: compile(value, true)});
116
121
  if (isCheckbox) el.checked = !!state[value];
117
122
  else if (isRadio) el.checked = el.value == state[value];
118
123
  else el.value = state[value] ?? "";
119
124
  }
120
-
121
125
  if (he(name,"hidden","visible")) {
122
126
  const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
123
127
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
124
128
  }
125
-
126
129
  if (name.startsWith(":")) {
127
130
  const fn = compile(value, true);
128
131
  trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
129
132
  }
130
-
131
133
  if (he(name,"init")) {
132
- compile(value, false)($, state, undefined, el, html, ...Object.values(data), ...[...refs.values()]);
134
+ execFn(value);
133
135
  } else if (name.startsWith("@") || name.startsWith("data-he")) {
134
136
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
135
137
  const [eventName, ...mods] = fullName.split(".");
@@ -138,40 +140,42 @@ const trackDependencies=fn=>{
138
140
  const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
139
141
  const debounceMod = mods.find(m => m.startsWith("debounce"));
140
142
  const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
141
-
142
143
  const _handler = e => {
143
- if (mods.includes("prevent")) e.preventDefault();
144
-
144
+ const exFn = v => compile(v, true)($, state, e, el, html,get,post,put,patch,del, ...Object.values(data), ...[...refs.values()])
145
+ if (mods.includes("prevent")) e.preventDefault();
145
146
  const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
146
- for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
147
-
147
+ for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
148
148
  if (["keydown","keyup","keypress"].includes(event)) {
149
149
  const last = mods[mods.length - 1];
150
150
  if (last && !["prevent","once","outside","document"].includes(last)) {
151
151
  const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
152
152
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
153
153
  }
154
- }
155
-
156
- if (!mods.includes("outside") || !el.contains(e.target)) {
154
+ }
155
+ if (!mods.includes("outside") || !el.contains(e.target)) {
157
156
  if (isHttpMethod) {
158
- const options = compile(el.getAttribute('data-he-options') || el.getAttribute('options') || '{}', true)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
159
- let paramsAttr = el.getAttribute('data-he-params') || el.getAttribute('params') || '{}';
160
-
157
+ const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
158
+ const [target, action] = (getAttr('target') || "").split(":");
159
+ const options = {
160
+ ...exFn(getAttr('options') || '{}'),
161
+ ...(target && { target }),
162
+ ...(action && { action }),
163
+ ...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
164
+ ...getAttr('loading') && { loading: getAttr('loading') }
165
+ };
166
+ let paramsAttr = getAttr('params') || '{}';
161
167
  if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
162
168
  const props = paramsAttr.split(":").map(s => s.trim());
163
169
  paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
164
170
  }
165
- const params = compile(paramsAttr, true)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
171
+ const params = exFn(paramsAttr);
166
172
  ajax(value, eventName.toUpperCase(), options, params);
167
173
  } else {
168
- compile(value, false)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
174
+ exFn(value);
169
175
  }
170
- }
171
-
176
+ }
172
177
  if (mods.includes("once")) receiver.removeEventListener(event, handler);
173
178
  };
174
-
175
179
  const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
176
180
  receiver.addEventListener(event, handler);
177
181
  if (!listeners.has(el)) listeners.set(el, []);
@@ -179,18 +183,15 @@ const trackDependencies=fn=>{
179
183
  }
180
184
  }
181
185
  });
182
-
183
186
  return newBindings;
184
187
  }
185
-
186
188
  new MutationObserver(ms=>{
187
- if(isUpdatingDOM)return;
188
- for(const m of ms){
189
- m.removedNodes.forEach(n=>n.nodeType==1&&cleanup(n));
190
- m.addedNodes.forEach(n=>n.nodeType==1&&!n.hasAttribute("data-he-p")&&processElements(n).forEach(applyBinding));
191
- }
192
- }).observe(root,{childList:1,subtree:1});
193
-
189
+ if(isUpdatingDOM)return;
190
+ for(const m of ms){
191
+ 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));
193
+ }
194
+ }).observe(root,{childList:1,subtree:1});
194
195
  processElements(root);
195
196
  for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
196
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {