sprae 1.0.0 → 2.0.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.
- package/.github/workflows/node.js.yml +30 -0
- package/package.json +7 -2
- package/plan.md +20 -6
- package/r&d.md +60 -9
- package/readme.md +121 -72
- package/sprae.js +644 -252
- package/sprae.min.js +1 -1
- package/src/core.js +30 -102
- package/src/directives.js +235 -0
- package/src/index.js +1 -1
- package/test/index.html +5 -2
- package/test/test.js +148 -71
- package/src/directives/aria.js +0 -10
- package/src/directives/common.js +0 -15
- package/src/directives/data.js +0 -10
- package/src/directives/if.js +0 -31
- package/src/directives/index.js +0 -11
- package/src/directives/on.js +0 -12
- package/src/directives/prop.js +0 -10
- package/src/directives/text.js +0 -10
- package/src/directives/value.js +0 -26
- package/src/directives/with.js +0 -21
package/sprae.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
function t(){throw new Error("Cycle detected")}function e(){if(o>1)o--;else{for(var t,e=!1;void 0!==n;){var i=n;for(n=void 0,s++;void 0!==i;){var r=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&h(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=r}}if(s=0,o--,e)throw t}}var r=void 0,n=void 0,o=0,s=0,f=0;function a(t){if(void 0!==r){var e=t.n;if(void 0===e||e.t!==r)return r.s=e={i:0,S:t,p:void 0,n:r.s,t:r,e:void 0,x:void 0,r:e},t.n=e,32&r.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=r.s,r.s.p=e,r.s=e),e}}function u(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function l(t){return new u(t)}function h(t){for(var e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function v(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function c(t){for(var e=t.s,i=void 0;void 0!==e;){var r=e.n;-1===e.i?(e.S.U(e),e.n=void 0):(void 0!==i&&(i.p=e),e.p=void 0,e.n=i,i=e),e.S.n=e.r,void 0!==e.r&&(e.r=void 0),e=r}t.s=i}function d(t){u.call(this,void 0),this.x=t,this.s=void 0,this.g=f-1,this.f=4}function p(t){return new d(t)}function y(t){var i=t.u;if(t.u=void 0,"function"==typeof i){o++;var n=r;r=void 0;try{i()}catch(e){throw t.f&=-2,t.f|=8,b(t),e}finally{r=n,e()}}}function b(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,y(t)}function m(t){if(r!==this)throw new Error("Out-of-order effect");c(this),r=t,this.f&=-2,8&this.f&&b(this),e()}function g(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function S(t){var e=new g(t);return e.c(),e.d.bind(e)}u.prototype.h=function(){return!0},u.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},u.prototype.U=function(t){var e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)},u.prototype.subscribe=function(t){var e=this;return S((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},u.prototype.valueOf=function(){return this.value},u.prototype.toString=function(){return this.value+""},u.prototype.peek=function(){return this.v},Object.defineProperty(u.prototype,"value",{get:function(){var t=a(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){s>100&&t(),this.v=i,this.i++,f++,o++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(d.prototype=new u).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===f)return!0;if(this.g=f,this.f|=1,this.i>0&&!h(this))return this.f&=-2,!0;var t=r;try{v(this),r=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return r=t,c(this),this.f&=-2,!0},d.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(var e=this.s;void 0!==e;e=e.n)e.S.S(e)}u.prototype.S.call(this,t)},d.prototype.U=function(t){if(u.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},d.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},d.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(d.prototype,"value",{get:function(){1&this.f&&t();var e=a(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),g.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},g.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,y(this),v(this),o++;var e=r;return r=this,m.bind(this,e)},g.prototype.N=function(){2&this.f||(this.f|=2,this.o=n,n=this)},g.prototype.d=function(){this.f|=8,1&this.f||b(this)},Symbol.observable||=Symbol("observable");var x=new FinalizationRegistry((t=>t.call?.())),w=t=>t&&t.peek,O=Symbol("signal-struct");function j(t){if(t[O])return t;let e,i;if(!k(t))throw Error("Only array or object states are supported");e={},i={};for(let r in t)i[r]=A(e,r,t[r]);return Object.defineProperty(e,O,{configurable:!1,enumerable:!1,value:!0}),Object.seal(e),e}function A(t,e,i){let r,n=w(i)?i:k(i)?l(j(i)):l((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,f,a,u,h,v;return r&&(f=t=>n.value=t,(s=i)&&(v=(s[Symbol.observable]?.()||s).subscribe?.(f,a,undefined),h=v&&(()=>v.unsubscribe?.())||s.set&&s.call?.(u,f)||(s.then?.((t=>{!u&&f(t)}),a)||(async t=>{try{for await(t of s){if(u)return;f(t)}}catch(t){}})())&&(t=>u=1),x.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:w(i)?t=>n.value=t:k(i)?t=>t?Object.assign(n.value,t):n.value=t:t=>n.value=t,enumerable:!0,configurable:!1}),n}function k(t){return t&&t.constructor===Object}var N=new WeakMap;function $(t,e){if(!t.children)return;if(N.has(t))return N.get(t);e||={};const i=j(e),r=t=>{let e,n;if(t.attributes)for(let r=0;r<t.attributes.length;){let o=t.attributes[r];if(":"===o.name[0]){if(e=E[o.name]||E.default,t.removeAttribute(o.name),n=!1===e(t,o.value,i,o.name.slice(1)))break}else r++}if(!n)for(let e of t.children)r(e)};return r(t),N.set(t,i),i}var E={},U=(t,e,i)=>{e.startsWith("on")&&(e=e.toLowerCase()),t[e]!==i&&(t[e]=i),!1===i||null==i?t.removeAttribute(e):"function"!=typeof i&&t.setAttribute(C(e),!0===i?"":"number"==typeof i||"string"==typeof i?i:"class"===e?(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" "):"style"===e?Object.entries(i).map((([t,e])=>`${t}: ${e}`)).join(";"):"")},W=document.createElement("div"),C=t=>{W.dataset[t]="";let e=W.attributes[0].name.slice(5);return delete W.dataset[t],e},B=(t,e,i,r=null)=>{let n,o,s,f=0,a=i.length,u=e.length,{remove:l,same:h,insert:v,replace:c}=B;for(;f<a&&f<u&&h(e[f],i[f]);)f++;for(;f<a&&f<u&&h(i[a-1],e[u-1]);)r=i[(--u,--a)];if(f==u)for(;f<a;)v(r,i[f++],t);else{for(n=e[f];f<a;)s=i[f++],o=n?n.nextSibling:r,h(n,s)?n=o:f<a&&h(i[f],o)?(c(n,s,t),n=o):v(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};B.same=(t,e)=>t==e,B.replace=(t,e,i)=>i.replaceChild(e,t),B.insert=(t,e,i)=>i.insertBefore(e,t),B.remove=(t,e)=>e.removeChild(t);var P=B,L={},M={},T={},F=t=>null===t?M:void 0===t?T:"number"==typeof t||t instanceof Number?L[t]||(L[t]=new Number(t)):"string"==typeof t||t instanceof String?L[t]||(L[t]=new String(t)):"boolean"==typeof t||t instanceof Boolean?L[t]||(L[t]=new Boolean(t)):t;E[":with"]=(t,i,r)=>{let n=R(i,"with",r);const s=p((()=>Object.assign({},r,n(r))));let f=$(t,s.value);S(((t=s.value)=>function(t){if(o>0)return t();o++;try{return t()}finally{e()}}((()=>Object.assign(f,t)))))},E[":if"]=(t,e,i)=>{let r=document.createTextNode(""),n=[R(e,":if",i)],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(R(e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let f=p((()=>n.findIndex((t=>t(i)))));return S(((t=f.value)=>o[t]!=s&&((s[I]||s).replaceWith(s=o[t]||r),$(s,i)))),!1};var I=Symbol(":each");E[":each"]=(t,e,r)=>{let n=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!n)return q(new Error,e);const o=R(n.items,":each",r),s=t[I]=document.createTextNode("");t.replaceWith(s);const f=p((()=>{let t=o(r);return"number"==typeof t?Array.from({length:t},((t,e)=>e+1)):t})),a=new WeakMap,u=new WeakMap;let l=[];return S(((e=f.value)=>{e||(e=[]);let o=[],h=[];for(let s of e){let e=F(s),f=u.get(e);if(f||(f=t.cloneNode(!0),u.set(e,f)),o.push(f),!a.has(e)){let t=Object.create(r);t[n.item]=s,n.index&&(t[n.index]=i),a.set(e,t)}h.push(a.get(e))}P(s.parentNode,l,o,s),l=o;for(let t=0;t<o.length;t++)$(o[t],h[t])})),!1},E.default=(t,e,i,r)=>{let n=R(e,":"+r,i);S((()=>{return e=n(i),U(t,r,e);var e}))},E[":aria"]=(t,e,i)=>{let r=R(e,":aria",i);S((()=>(e=>{for(let i in e)U(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))},E[":data"]=(t,e,i)=>{let r=R(e,":data",i);const n=p((()=>r(i)));S(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},E[":on"]=(t,e,i)=>{let r,n=R(e,":on",i),o=p((()=>n(i)));S(((e=o.value)=>{for(let e in r)t.removeEventListener(e,r[e]);r=e;for(let e in r)t.addEventListener(e,r[e])}))},E[":"]=(t,e,i)=>{let r=R(e,":",i);S((()=>(e=>{if(e)for(let i in e)U(t,i,e[i])})(r(i))))},E[":text"]=(t,e,i)=>{let r=R(e,":text",i);S((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},E[":value"]=(t,e,i)=>{let r=R(e,":in",i),[n,o]=(t=>["checkbox"===t.type?()=>t.checked:()=>t.value,"text"===t.type||""===t.type?e=>t.value=null==e?"":e:"checkbox"===t.type?e=>(t.value=e?"on":"",U(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e])(t);S((()=>{return e=r(i),U(t,"value",e),void o(e);var e}))};var z={};function R(t,e,i){if(z[t])return z[t];let r,n=/^[\n\s]*if.*\(.*\)/.test(t)||/^(let|const)\s/.test(t)?`(() => { ${t} })()`:t;try{r=new Function(["scope"],`let result; with (scope) { result = ${n} }; return result;`)}catch(r){return q(r,t,e,i)}return z[t]=n=>{let o;try{o=r(n)}catch(r){return q(r,t,e,i)}return o}}function q(t,e,i,r){Object.assign(t,{expression:e}),console.warn(`∴sprae: ${t.message}\n\n${i}=${e?`"${e}"\n\n`:""}`,r),setTimeout((()=>{throw t}),0)}var D=$;export{D as default,E as directives};
|
package/src/core.js
CHANGED
|
@@ -1,110 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import signalStruct from 'signal-struct';
|
|
2
2
|
|
|
3
|
-
let curEl, curDir;
|
|
4
3
|
// sprae element: apply directives
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
const memo = new WeakMap
|
|
5
|
+
export default function sprae(container, values) {
|
|
6
|
+
if (!container.children) return
|
|
7
|
+
if (memo.has(container)) return memo.get(container)
|
|
8
|
+
|
|
9
|
+
values ||= {};
|
|
10
|
+
|
|
11
|
+
const state = signalStruct(values);
|
|
12
|
+
|
|
13
|
+
// init directives on element
|
|
14
|
+
const init = (el) => {
|
|
15
|
+
let dir, stop
|
|
16
|
+
if (el.attributes) {
|
|
17
|
+
for (let i = 0; i < el.attributes.length;) {
|
|
18
|
+
let attr = el.attributes[i]
|
|
19
|
+
if (attr.name[0]===':') {
|
|
20
|
+
dir = directives[attr.name] || directives.default
|
|
21
|
+
el.removeAttribute(attr.name)
|
|
22
|
+
if (stop = (dir(el, attr.value, state, attr.name.slice(1))===false)) break
|
|
23
|
+
}
|
|
24
|
+
else i++
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
const values = rsube(initScope);
|
|
28
|
-
ready = true;
|
|
29
|
-
|
|
30
|
-
// prepare directives - need to be after subscribing to values to get init state here
|
|
31
|
-
for (let name in directives) {
|
|
32
|
-
// updates[dir] = directives[dir](el)
|
|
33
|
-
const sel = `[${name.replace(':','\\:')}]`,
|
|
34
|
-
initDirective = directives[name]
|
|
35
|
-
|
|
36
|
-
// FIXME: possibly linear init of directives is better, who knows
|
|
37
|
-
const els = [...el.querySelectorAll(sel)];
|
|
38
|
-
if (el.matches?.(sel)) els.unshift(el);
|
|
39
|
-
|
|
40
|
-
let update
|
|
41
|
-
for (let el of els) if (update = initDirective(el, values)) updates.push(update);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
update(values);
|
|
45
|
-
|
|
46
|
-
// return update via destructuring of result to allow batch-update
|
|
47
|
-
values[Symbol.iterator] = function*(){ yield proxy; yield (diff) => update(Object.assign(values, diff)); };
|
|
48
|
-
|
|
49
|
-
const proxy = new Proxy(values, {
|
|
50
|
-
set: (s, k, v) => (values[k]=v, update(values), 1),
|
|
51
|
-
deleteProperty: (s, k) => (values[k]=undefined, update(values), 1)
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
return proxy
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// dict of directives
|
|
58
|
-
const directives = {}
|
|
59
|
-
|
|
60
|
-
// register a directive
|
|
61
|
-
export const directive = (name, initializer) => {
|
|
62
|
-
const className = name.replace(':','∴')
|
|
63
|
-
|
|
64
|
-
// create initializer of a directive on an element
|
|
65
|
-
return directives[name] = (el, initValues) => {
|
|
66
|
-
if (el.classList.contains(className)) return
|
|
67
|
-
el.classList.add(className)
|
|
68
|
-
let expr = el.getAttribute(name)
|
|
69
|
-
el.removeAttribute(name)
|
|
70
|
-
return initializer(el, expr, initValues);
|
|
27
|
+
if (!stop) for (let child of el.children) init(child)
|
|
71
28
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const registry = new FinalizationRegistry(unsub => unsub?.call?.())
|
|
75
|
-
|
|
76
|
-
let evaluatorMemo = {}
|
|
77
|
-
|
|
78
|
-
// borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
|
|
79
|
-
// it seems to be more robust than subscript
|
|
80
|
-
export function parseExpr(expression) {
|
|
81
|
-
if (evaluatorMemo[expression]) return evaluatorMemo[expression]
|
|
29
|
+
init(container)
|
|
82
30
|
|
|
83
|
-
|
|
84
|
-
// Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
|
|
85
|
-
// calling function so that we don't throw an error AND a "return" statement can b e used.
|
|
86
|
-
let rightSideSafeExpression = 0
|
|
87
|
-
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|
|
88
|
-
|| /^[\n\s]*if.*\(.*\)/.test(expression)
|
|
89
|
-
// Support expressions starting with "let/const" like: "let foo = 'bar'"
|
|
90
|
-
|| /^(let|const)\s/.test(expression)
|
|
91
|
-
? `(() => { ${expression} })()`
|
|
92
|
-
: expression
|
|
31
|
+
memo.set(container, state);
|
|
93
32
|
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
return new Function(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
|
|
97
|
-
} catch ( e ) {
|
|
98
|
-
return exprError(e, expression)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return evaluatorMemo[expression] = safeFunction()
|
|
33
|
+
return state;
|
|
103
34
|
}
|
|
104
35
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
setTimeout(() => { throw error }, 0)
|
|
109
|
-
return Promise.resolve()
|
|
110
|
-
}
|
|
36
|
+
|
|
37
|
+
// dict of directives
|
|
38
|
+
export const directives = {}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// directives & parsing
|
|
2
|
+
import sprae, { directives } from './core.js'
|
|
3
|
+
import { prop, input } from 'element-props'
|
|
4
|
+
// import { effect, computed, batch } from 'usignal'
|
|
5
|
+
import { effect, computed, batch } from '@preact/signals-core'
|
|
6
|
+
import swap from 'swapdom'
|
|
7
|
+
import p from 'primitive-pool'
|
|
8
|
+
|
|
9
|
+
directives[':with'] = (el, expr, rootState) => {
|
|
10
|
+
let evaluate = parseExpr(expr, 'with', rootState)
|
|
11
|
+
|
|
12
|
+
// Instead of extending signals (which is a bit hard since signal-struct internals is not uniform)
|
|
13
|
+
// we bind updating
|
|
14
|
+
const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
|
|
15
|
+
let state = sprae(el, params.value)
|
|
16
|
+
effect((values=params.value) => batch(()=>Object.assign(state, values)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
directives[':if'] = (el, expr, state) => {
|
|
20
|
+
let holder = document.createTextNode(''),
|
|
21
|
+
clauses = [parseExpr(expr, ':if', state)],
|
|
22
|
+
els = [el], cur = el
|
|
23
|
+
|
|
24
|
+
while (cur = el.nextElementSibling) {
|
|
25
|
+
if (cur.hasAttribute(':else')) {
|
|
26
|
+
cur.removeAttribute(':else');
|
|
27
|
+
if (expr = cur.getAttribute(':if')) {
|
|
28
|
+
cur.removeAttribute(':if'), cur.remove();
|
|
29
|
+
els.push(cur); clauses.push(parseExpr(expr, ':else :if', state));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
cur.remove(); els.push(cur); clauses.push(() => 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
el.replaceWith(cur = holder)
|
|
39
|
+
let idx = computed(() => clauses.findIndex(f => f(state)))
|
|
40
|
+
// NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
|
|
41
|
+
effect((i=idx.value) => (els[i] != cur && ((cur[_eachHolder]||cur).replaceWith(cur = els[i] || holder), sprae(cur, state))))
|
|
42
|
+
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const _eachHolder = Symbol(':each')
|
|
47
|
+
directives[':each'] = (tpl, expr, state) => {
|
|
48
|
+
let each = parseForExpression(expr);
|
|
49
|
+
if (!each) return exprError(new Error, expr);
|
|
50
|
+
|
|
51
|
+
const getItems = parseExpr(each.items, ':each', state);
|
|
52
|
+
|
|
53
|
+
// FIXME: make sure no memory leak here
|
|
54
|
+
// we need holder to be able :if replace it instead of tpl for combined case
|
|
55
|
+
const holder = tpl[_eachHolder] = document.createTextNode('')
|
|
56
|
+
tpl.replaceWith(holder)
|
|
57
|
+
|
|
58
|
+
const items = computed(()=>{
|
|
59
|
+
let list = getItems(state)
|
|
60
|
+
if (typeof list === 'number') return Array.from({length: list}, (_, i)=>i+1)
|
|
61
|
+
return list
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// stores scope per data item
|
|
65
|
+
const scopes = new WeakMap()
|
|
66
|
+
// element per data item
|
|
67
|
+
const itemEls = new WeakMap()
|
|
68
|
+
let curEls = []
|
|
69
|
+
effect((list=items.value) => {
|
|
70
|
+
if (!list) list = []
|
|
71
|
+
// collect elements/scopes for items
|
|
72
|
+
let newEls = [], elScopes = []
|
|
73
|
+
|
|
74
|
+
for (let item of list) {
|
|
75
|
+
let key = p(item)
|
|
76
|
+
let el = itemEls.get(key)
|
|
77
|
+
if (!el) {
|
|
78
|
+
el = tpl.cloneNode(true)
|
|
79
|
+
itemEls.set(key, el)
|
|
80
|
+
}
|
|
81
|
+
newEls.push(el)
|
|
82
|
+
|
|
83
|
+
if (!scopes.has(key)) {
|
|
84
|
+
let scope = Object.create(state)
|
|
85
|
+
scope[each.item] = item
|
|
86
|
+
if (each.index) scope[each.index] = i;
|
|
87
|
+
scopes.set(key, scope)
|
|
88
|
+
}
|
|
89
|
+
elScopes.push(scopes.get(key))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// swap is really fast & tiny
|
|
93
|
+
swap(holder.parentNode, curEls, newEls, holder)
|
|
94
|
+
curEls = newEls
|
|
95
|
+
|
|
96
|
+
// init new elements
|
|
97
|
+
for (let i = 0; i < newEls.length; i++) {
|
|
98
|
+
sprae(newEls[i], elScopes[i])
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
|
|
106
|
+
function parseForExpression(expression) {
|
|
107
|
+
let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
|
108
|
+
let stripParensRE = /^\s*\(|\)\s*$/g
|
|
109
|
+
let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
|
|
110
|
+
let inMatch = expression.match(forAliasRE)
|
|
111
|
+
|
|
112
|
+
if (!inMatch) return
|
|
113
|
+
|
|
114
|
+
let res = {}
|
|
115
|
+
res.items = inMatch[2].trim()
|
|
116
|
+
let item = inMatch[1].replace(stripParensRE, '').trim()
|
|
117
|
+
let iteratorMatch = item.match(forIteratorRE)
|
|
118
|
+
|
|
119
|
+
if (iteratorMatch) {
|
|
120
|
+
res.item = item.replace(forIteratorRE, '').trim()
|
|
121
|
+
res.index = iteratorMatch[1].trim()
|
|
122
|
+
} else {
|
|
123
|
+
res.item = item
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return res
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
// common-setter directives
|
|
131
|
+
directives['default'] = (el, expr, state, name) => {
|
|
132
|
+
let evaluate = parseExpr(expr, ':'+name, state)
|
|
133
|
+
const update = value => prop(el, name, value)
|
|
134
|
+
effect(() => update(evaluate(state)))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
directives[':aria'] = (el, expr, state) => {
|
|
138
|
+
let evaluate = parseExpr(expr, ':aria', state)
|
|
139
|
+
const update = (value) => {
|
|
140
|
+
for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key] == null ? null : value[key] + '');
|
|
141
|
+
}
|
|
142
|
+
effect(() => update(evaluate(state)))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
directives[':data'] = (el, expr, state) => {
|
|
146
|
+
let evaluate = parseExpr(expr, ':data', state)
|
|
147
|
+
const value = computed(() => evaluate(state))
|
|
148
|
+
effect((v=value.value) => {
|
|
149
|
+
for (let key in v) el.dataset[key] = v[key];
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
directives[':on'] = (el, expr, state) => {
|
|
154
|
+
let evaluate = parseExpr(expr, ':on', state)
|
|
155
|
+
let listeners = computed(() => evaluate(state))
|
|
156
|
+
let prevListeners
|
|
157
|
+
effect((values=listeners.value) => {
|
|
158
|
+
for (let evt in prevListeners) el.removeEventListener(evt, prevListeners[evt]);
|
|
159
|
+
prevListeners = values;
|
|
160
|
+
for (let evt in prevListeners) el.addEventListener(evt, prevListeners[evt]);
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
directives[':'] = (el, expr, state) => {
|
|
165
|
+
let evaluate = parseExpr(expr, ':', state)
|
|
166
|
+
const update = (value) => {
|
|
167
|
+
if (!value) return
|
|
168
|
+
for (let key in value) prop(el, key, value[key]);
|
|
169
|
+
}
|
|
170
|
+
effect(()=>update(evaluate(state)))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
directives[':text'] = (el, expr, state) => {
|
|
174
|
+
let evaluate = parseExpr(expr, ':text', state)
|
|
175
|
+
|
|
176
|
+
const update = (value) => {
|
|
177
|
+
el.textContent = value == null ? '' : value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
effect(()=>update(evaluate(state)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// connect expr to element value
|
|
184
|
+
directives[':value'] = (el, expr, state) => {
|
|
185
|
+
let evaluate = parseExpr(expr, ':in', state)
|
|
186
|
+
|
|
187
|
+
let [get, set] = input(el);
|
|
188
|
+
|
|
189
|
+
const update = (value) => {
|
|
190
|
+
prop(el, 'value', value)
|
|
191
|
+
set(value);
|
|
192
|
+
}
|
|
193
|
+
effect(()=>update(evaluate(state)))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let evaluatorMemo = {}
|
|
197
|
+
|
|
198
|
+
// borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
|
|
199
|
+
// it seems to be more robust than subscript
|
|
200
|
+
function parseExpr(expression, dir, scope) {
|
|
201
|
+
if (evaluatorMemo[expression]) return evaluatorMemo[expression]
|
|
202
|
+
|
|
203
|
+
// Some expressions that are useful in Alpine are not valid as the right side of an expression.
|
|
204
|
+
// Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
|
|
205
|
+
// calling function so that we don't throw an error AND a "return" statement can b e used.
|
|
206
|
+
let rightSideSafeExpression = 0
|
|
207
|
+
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|
|
208
|
+
|| /^[\n\s]*if.*\(.*\)/.test(expression)
|
|
209
|
+
// Support expressions starting with "let/const" like: "let foo = 'bar'"
|
|
210
|
+
|| /^(let|const)\s/.test(expression)
|
|
211
|
+
? `(() => { ${expression} })()`
|
|
212
|
+
: expression;
|
|
213
|
+
|
|
214
|
+
// guard static-time eval errors
|
|
215
|
+
let evaluate
|
|
216
|
+
try {
|
|
217
|
+
evaluate = new Function(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
|
|
218
|
+
} catch ( e ) {
|
|
219
|
+
return exprError(e, expression, dir, scope)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// guard runtime eval errors
|
|
223
|
+
return evaluatorMemo[expression] = (state) => {
|
|
224
|
+
let result
|
|
225
|
+
try { result = evaluate(state) }
|
|
226
|
+
catch (e) { return exprError(e, expression, dir, scope) }
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function exprError(error, expression, dir, scope) {
|
|
232
|
+
Object.assign( error, { expression } )
|
|
233
|
+
console.warn(`∴sprae: ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, scope)
|
|
234
|
+
setTimeout(() => { throw error }, 0)
|
|
235
|
+
}
|
package/src/index.js
CHANGED
package/test/index.html
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
"value-ref" : "../node_modules/value-ref/value-ref.js",
|
|
13
13
|
"hyperf" : "../node_modules/hyperf/hyperf.js",
|
|
14
14
|
"sube" : "../node_modules/sube/sube.js",
|
|
15
|
+
"signal-struct" : "../node_modules/signal-struct/signal-struct.js",
|
|
15
16
|
"element-props" : "../node_modules/element-props/element-props.js",
|
|
17
|
+
"swapdom" : "../node_modules/swapdom/swap-inflate.js",
|
|
18
|
+
"primitive-pool" : "../node_modules/primitive-pool/index.js",
|
|
16
19
|
"preact": "../node_modules/preact/dist/preact.mjs",
|
|
17
20
|
"preact/hooks": "../node_modules/preact/hooks/dist/hooks.mjs",
|
|
18
|
-
"@preact/signals": "../node_modules/@preact/signals/dist/signals.mjs",
|
|
19
|
-
"
|
|
21
|
+
"@preact/signals-core": "../node_modules/@preact/signals-core/dist/signals-core.mjs",
|
|
22
|
+
"usignal": "../node_modules/usignal/esm/sync.js"
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
</script>
|