sprae 2.0.0 → 2.1.1
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/examples/todomvc.html +100 -0
- package/package.json +5 -6
- package/plan.md +9 -1
- package/r&d.md +63 -8
- package/readme.md +43 -20
- package/sprae.js +205 -178
- package/sprae.min.js +1 -1
- package/src/core.js +16 -8
- package/src/directives.js +76 -52
- package/src/index.js +0 -2
- package/test/index.html +1 -1
- package/test/test.js +47 -5
- package/src/directives/each.js +0 -53
package/sprae.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function t(){throw new Error("Cycle detected")}function e(){if(
|
|
1
|
+
function t(){throw new Error("Cycle detected")}function e(){if(n>1)n--;else{for(var t,e=!1;void 0!==r;){var i=r;for(r=void 0,o++;void 0!==i;){var s=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&l(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(o=0,n--,e)throw t}}var i=void 0,r=void 0,n=0,o=0,s=0;function a(t){if(void 0!==i){var e=t.n;if(void 0===e||e.t!==i)return i.s=e={i:0,S:t,p:void 0,n:i.s,t:i,e:void 0,x:void 0,r:e},t.n=e,32&i.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=i.s,i.s.p=e,i.s=e),e}}function f(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new f(t)}function l(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 h(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 v(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 c(t){f.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function d(t){return new c(t)}function p(t){var r=t.u;if(t.u=void 0,"function"==typeof r){n++;var o=i;i=void 0;try{r()}catch(e){throw t.f&=-2,t.f|=8,b(t),e}finally{i=o,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,p(t)}function y(t){if(i!==this)throw new Error("Out-of-order effect");v(this),i=t,this.f&=-2,8&this.f&&b(this),e()}function m(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function g(t){var e=new m(t);return e.c(),e.d.bind(e)}f.prototype.h=function(){return!0},f.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)},f.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)},f.prototype.subscribe=function(t){var e=this;return g((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},f.prototype.valueOf=function(){return this.value},f.prototype.toString=function(){return this.value+""},f.prototype.peek=function(){return this.v},Object.defineProperty(f.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){o>100&&t(),this.v=i,this.i++,s++,n++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(c.prototype=new f).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===s)return!0;if(this.g=s,this.f|=1,this.i>0&&!l(this))return this.f&=-2,!0;var t=i;try{h(this),i=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 i=t,v(this),this.f&=-2,!0},c.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)}f.prototype.S.call(this,t)},c.prototype.U=function(t){if(f.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)}},c.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},c.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(c.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}}),m.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},m.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,p(this),h(this),n++;var e=i;return i=this,y.bind(this,e)},m.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},m.prototype.d=function(){this.f|=8,1&this.f||b(this)},Symbol.observable||=Symbol("observable");var S=new FinalizationRegistry((t=>t.call?.())),x=t=>t&&t.peek,w=Symbol("signal-struct");function A(t){if((e=t)&&e[w])return t;var e;let i,r;if(j(t)){i={},r={};for(let e in t)r[e]=O(i,e,t[e]);return Object.defineProperty(i,w,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>A(t))):t}function O(t,e,i){let r,n=x(i)?i:j(i)||Array.isArray(i)?u(A(i)):u((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,a,f,l,h,v;return r&&(a=t=>n.value=t,(s=i)&&(v=(s[Symbol.observable]?.()||s).subscribe?.(a,f,undefined),h=v&&(()=>v.unsubscribe?.())||s.set&&s.call?.(l,a)||(s.then?.((t=>{!l&&a(t)}),f)||(async t=>{try{for await(t of s){if(l)return;a(t)}}catch(t){}})())&&(t=>l=1),S.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:!x(i)&&j(i)?t=>t?Object.assign(n.value,t):n.value=A(t):t=>n.value=A(t),enumerable:!0,configurable:!1}),n}function j(t){return t&&t.constructor===Object}var k=(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($(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(";"):"")},N=document.createElement("div"),$=t=>{N.dataset[t]="";let e=N.attributes[0].name.slice(5);return delete N.dataset[t],e},E=(t,e,i,r=null)=>{let n,o,s,a=0,f=i.length,u=e.length,{remove:l,same:h,insert:v,replace:c}=E;for(;a<f&&a<u&&h(e[a],i[a]);)a++;for(;a<f&&a<u&&h(i[f-1],e[u-1]);)r=i[(--u,--f)];if(a==u)for(;a<f;)v(r,i[a++],t);else{for(n=e[a];a<f;)s=i[a++],o=n?n.nextSibling:r,h(n,s)?n=o:a<f&&h(i[a],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};E.same=(t,e)=>t==e,E.replace=(t,e,i)=>i.replaceChild(e,t),E.insert=(t,e,i)=>i.insertBefore(e,t),E.remove=(t,e)=>e.removeChild(t);var U=E,W={},C={},B={},P=(t,e,i,r)=>{let n=I(e,":"+r,i);g((()=>{return e=n(i),k(t,r,e);var e}))},L={},M=Symbol(":each"),T=Symbol(":ref");L[":with"]=(t,i,r)=>{let o=I(i,"with",r);const s=d((()=>Object.assign({},r,o(r))));let a=q(t,s.value);return g(((t=s.value)=>function(t){if(n>0)return t();n++;try{return t()}finally{e()}}((()=>Object.assign(a,t))))),!1},L[":ref"]=(t,e,i)=>t.hasAttribute(":each")?t[T]=e:(q(t,Object.assign(Object.create(i),{[e]:t})),!1),L[":if"]=(t,e,i)=>{let r=document.createTextNode(""),n=[I(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(I(e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let a=d((()=>n.findIndex((t=>t(i)))));return g(((t=a.value)=>o[t]!=s&&((s[M]||s).replaceWith(s=o[t]||r),q(s,i)))),!1},L[":each"]=(t,e,i)=>{let r=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(!r)return z(new Error,e);const n=I(r.items,":each",i),o=t[M]=document.createTextNode("");t.replaceWith(o);const s=d((()=>{let t=n(i);return t?"number"==typeof t?Array.from({length:t},((t,e)=>[e,e+1])):t.constructor===Object?Object.entries(t):Array.isArray(t)?t.map(((t,e)=>[e+1,t])):void z(Error("Bad list value"),r.items,":each",t):[]})),a=new WeakMap,f=new WeakMap;let u=[];return g(((e=s.value)=>{let n=[],l=[];for(let[o,s]of e){let e=null===(h=s)?C:void 0===h?B:"number"==typeof h||h instanceof Number?W[h]||(W[h]=new Number(h)):"string"==typeof h||h instanceof String?W[h]||(W[h]=new String(h)):"boolean"==typeof h||h instanceof Boolean?W[h]||(W[h]=new Boolean(h)):h,u=f.get(e);if(u||(u=t.cloneNode(!0),f.set(e,u)),n.push(u),!a.has(e)){let n=Object.create(i);n[r.item]=s,r.index&&(n[r.index]=o),t[T]&&(n[t[T]]=u),a.set(e,n)}l.push(a.get(e))}var h;U(o.parentNode,u,n,o),u=n;for(let t=0;t<n.length;t++)q(n[t],l[t])})),!1},L[":id"]=(t,e,i)=>{let r=I(e,":id",i);g((()=>{return e=r(i),t.id=e||0===e?e:"";var e}))},L[":"]=(t,e,i)=>{let r=I(e,":",i);g((()=>(e=>{if(e)for(let i in e)k(t,i,e[i])})(r(i))))},L[":text"]=(t,e,i)=>{let r=I(e,":text",i);g((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},L[":value"]=(t,e,i)=>{let r=I(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":"",k(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);g((()=>{return e=r(i),k(t,"value",e),void o(e);var e}))},L[":on"]=(t,e,i)=>{let r,n=I(e,":on",i),o=d((()=>n(i)));g(((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])}))},L[":data"]=(t,e,i)=>{let r=I(e,":data",i);const n=d((()=>r(i)));g(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},L[":aria"]=(t,e,i)=>{let r=I(e,":aria",i);g((()=>(e=>{for(let i in e)k(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))};var F={};function I(t,e,i){if(F[t])return F[t];let r,n=/^[\n\s]*if.*\(.*\)/.test(t)||/^(let|const)\s/.test(t)?`(() => { ${t} })()`:t;try{r=new Function(`let result; with (arguments[0]) { result = (${n}) }; return result;`)}catch(r){return z(r,t,e,i)}return F[t]=n=>{let o;try{o=r(n)}catch(r){return z(r,t,e,i)}return o}}function z(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 R=new WeakMap;function q(t,e){if(!t.children)return;if(R.has(t))return R.get(t);e||={};const i=A(e),r=t=>{let e,n;if(t.attributes){for(let r in L)if((n=t.attributes[r])&&(e=L[r],t.removeAttribute(r),!1===e(t,n.value,i)))return;for(let e=0;e<t.attributes.length;){let r=t.attributes[e];if(":"===r.name[0]){if(t.removeAttribute(r.name),!1===P(t,r.value,i,r.name.slice(1)))return}else e++}}for(let e of t.children)r(e)};return r(t),R.set(t,i),i}var D=q;export{D as default};
|
package/src/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import signalStruct from 'signal-struct';
|
|
2
|
+
import defaultDirective, { directives } from './directives.js';
|
|
2
3
|
|
|
3
4
|
// sprae element: apply directives
|
|
4
5
|
const memo = new WeakMap
|
|
@@ -12,27 +13,34 @@ export default function sprae(container, values) {
|
|
|
12
13
|
|
|
13
14
|
// init directives on element
|
|
14
15
|
const init = (el) => {
|
|
15
|
-
let dir,
|
|
16
|
+
let dir, attr
|
|
17
|
+
|
|
16
18
|
if (el.attributes) {
|
|
19
|
+
// directives must be initialized in order
|
|
20
|
+
for (let name in directives) {
|
|
21
|
+
if (attr = el.attributes[name]) {
|
|
22
|
+
dir = directives[name]
|
|
23
|
+
el.removeAttribute(name)
|
|
24
|
+
if (dir(el, attr.value, state) === false) return
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
for (let i = 0; i < el.attributes.length;) {
|
|
18
29
|
let attr = el.attributes[i]
|
|
19
30
|
if (attr.name[0]===':') {
|
|
20
|
-
dir = directives[attr.name] || directives.default
|
|
21
31
|
el.removeAttribute(attr.name)
|
|
22
|
-
if (
|
|
32
|
+
if (defaultDirective(el, attr.value, state, attr.name.slice(1)) === false) return
|
|
23
33
|
}
|
|
24
34
|
else i++
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
|
-
|
|
37
|
+
|
|
38
|
+
for (let child of el.children) init(child)
|
|
28
39
|
}
|
|
40
|
+
|
|
29
41
|
init(container)
|
|
30
42
|
|
|
31
43
|
memo.set(container, state);
|
|
32
44
|
|
|
33
45
|
return state;
|
|
34
46
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// dict of directives
|
|
38
|
-
export const directives = {}
|
package/src/directives.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
// directives & parsing
|
|
2
|
-
import sprae
|
|
2
|
+
import sprae from './core.js'
|
|
3
3
|
import { prop, input } from 'element-props'
|
|
4
|
-
// import { effect, computed, batch } from 'usignal'
|
|
4
|
+
// import { effect, computed, batch } from 'usignal/sync'
|
|
5
5
|
import { effect, computed, batch } from '@preact/signals-core'
|
|
6
6
|
import swap from 'swapdom'
|
|
7
7
|
import p from 'primitive-pool'
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
// any-prop directives
|
|
11
|
+
export default (el, expr, state, name) => {
|
|
12
|
+
let evaluate = parseExpr(expr, ':'+name, state)
|
|
13
|
+
const update = value => prop(el, name, value)
|
|
14
|
+
effect(() => update(evaluate(state)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// reserved directives - order matters!
|
|
18
|
+
export const directives = {}
|
|
19
|
+
|
|
20
|
+
const _each = Symbol(':each'), _ref = Symbol(':ref')
|
|
21
|
+
|
|
9
22
|
directives[':with'] = (el, expr, rootState) => {
|
|
10
23
|
let evaluate = parseExpr(expr, 'with', rootState)
|
|
11
24
|
|
|
@@ -13,7 +26,16 @@ directives[':with'] = (el, expr, rootState) => {
|
|
|
13
26
|
// we bind updating
|
|
14
27
|
const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
|
|
15
28
|
let state = sprae(el, params.value)
|
|
16
|
-
effect((values=params.value) => batch(()=>Object.assign(state, values)))
|
|
29
|
+
effect((values=params.value) => batch(() => Object.assign(state, values)))
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
directives[':ref'] = (el, expr, state) => {
|
|
34
|
+
// make sure :ref is initialized after :each
|
|
35
|
+
if (el.hasAttribute(':each')) return el[_ref] = expr;
|
|
36
|
+
|
|
37
|
+
sprae(el, Object.assign(Object.create(state), {[expr]: el}))
|
|
38
|
+
return false
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
directives[':if'] = (el, expr, state) => {
|
|
@@ -38,12 +60,11 @@ directives[':if'] = (el, expr, state) => {
|
|
|
38
60
|
el.replaceWith(cur = holder)
|
|
39
61
|
let idx = computed(() => clauses.findIndex(f => f(state)))
|
|
40
62
|
// NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
|
|
41
|
-
effect((i=idx.value) => (els[i] != cur && ((cur[
|
|
63
|
+
effect((i=idx.value) => (els[i] != cur && ((cur[_each]||cur).replaceWith(cur = els[i] || holder), sprae(cur, state))))
|
|
42
64
|
|
|
43
65
|
return false
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
const _eachHolder = Symbol(':each')
|
|
47
68
|
directives[':each'] = (tpl, expr, state) => {
|
|
48
69
|
let each = parseForExpression(expr);
|
|
49
70
|
if (!each) return exprError(new Error, expr);
|
|
@@ -51,14 +72,17 @@ directives[':each'] = (tpl, expr, state) => {
|
|
|
51
72
|
const getItems = parseExpr(each.items, ':each', state);
|
|
52
73
|
|
|
53
74
|
// FIXME: make sure no memory leak here
|
|
54
|
-
// we need
|
|
55
|
-
const holder = tpl[
|
|
75
|
+
// we need :if to be able to replace holder instead of tpl for :if :each case
|
|
76
|
+
const holder = tpl[_each] = document.createTextNode('')
|
|
56
77
|
tpl.replaceWith(holder)
|
|
57
78
|
|
|
58
79
|
const items = computed(()=>{
|
|
59
80
|
let list = getItems(state)
|
|
60
|
-
if (
|
|
61
|
-
return list
|
|
81
|
+
if (!list) return []
|
|
82
|
+
if (typeof list === 'number') return Array.from({length: list}, (_, i)=>[i, i+1])
|
|
83
|
+
if (list.constructor === Object) return Object.entries(list)
|
|
84
|
+
if (Array.isArray(list)) return list.map((item,i) => [i+1, item])
|
|
85
|
+
exprError(Error('Bad list value'), each.items, ':each', list)
|
|
62
86
|
})
|
|
63
87
|
|
|
64
88
|
// stores scope per data item
|
|
@@ -67,26 +91,27 @@ directives[':each'] = (tpl, expr, state) => {
|
|
|
67
91
|
const itemEls = new WeakMap()
|
|
68
92
|
let curEls = []
|
|
69
93
|
effect((list=items.value) => {
|
|
70
|
-
if (!list) list = []
|
|
71
94
|
// collect elements/scopes for items
|
|
72
95
|
let newEls = [], elScopes = []
|
|
73
96
|
|
|
74
|
-
for (let item of list) {
|
|
75
|
-
let
|
|
76
|
-
let el = itemEls.get(
|
|
97
|
+
for (let [idx, item] of list) {
|
|
98
|
+
let itemKey = p(item)
|
|
99
|
+
let el = itemEls.get(itemKey)
|
|
77
100
|
if (!el) {
|
|
78
101
|
el = tpl.cloneNode(true)
|
|
79
|
-
itemEls.set(
|
|
102
|
+
itemEls.set(itemKey, el)
|
|
80
103
|
}
|
|
81
104
|
newEls.push(el)
|
|
82
105
|
|
|
83
|
-
if (!scopes.has(
|
|
106
|
+
if (!scopes.has(itemKey)) {
|
|
84
107
|
let scope = Object.create(state)
|
|
85
108
|
scope[each.item] = item
|
|
86
|
-
if (each.index) scope[each.index] =
|
|
87
|
-
|
|
109
|
+
if (each.index) scope[each.index] = idx;
|
|
110
|
+
// provide ref, if indicated
|
|
111
|
+
if (tpl[_ref]) scope[tpl[_ref]] = el
|
|
112
|
+
scopes.set(itemKey, scope)
|
|
88
113
|
}
|
|
89
|
-
elScopes.push(scopes.get(
|
|
114
|
+
elScopes.push(scopes.get(itemKey))
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
// swap is really fast & tiny
|
|
@@ -126,39 +151,10 @@ function parseForExpression(expression) {
|
|
|
126
151
|
return res
|
|
127
152
|
}
|
|
128
153
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
})
|
|
154
|
+
directives[':id'] = (el, expr, state) => {
|
|
155
|
+
let evaluate = parseExpr(expr, ':id', state)
|
|
156
|
+
const update = v => el.id = v || v === 0 ? v : ''
|
|
157
|
+
effect(()=>update(evaluate(state)))
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
directives[':'] = (el, expr, state) => {
|
|
@@ -193,6 +189,34 @@ directives[':value'] = (el, expr, state) => {
|
|
|
193
189
|
effect(()=>update(evaluate(state)))
|
|
194
190
|
}
|
|
195
191
|
|
|
192
|
+
directives[':on'] = (el, expr, state) => {
|
|
193
|
+
let evaluate = parseExpr(expr, ':on', state)
|
|
194
|
+
let listeners = computed(() => evaluate(state))
|
|
195
|
+
let prevListeners
|
|
196
|
+
effect((values=listeners.value) => {
|
|
197
|
+
for (let evt in prevListeners) el.removeEventListener(evt, prevListeners[evt]);
|
|
198
|
+
prevListeners = values;
|
|
199
|
+
for (let evt in prevListeners) el.addEventListener(evt, prevListeners[evt]);
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
directives[':data'] = (el, expr, state) => {
|
|
204
|
+
let evaluate = parseExpr(expr, ':data', state)
|
|
205
|
+
const value = computed(() => evaluate(state))
|
|
206
|
+
effect((v=value.value) => {
|
|
207
|
+
for (let key in v) el.dataset[key] = v[key];
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
directives[':aria'] = (el, expr, state) => {
|
|
212
|
+
let evaluate = parseExpr(expr, ':aria', state)
|
|
213
|
+
const update = (value) => {
|
|
214
|
+
for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key] == null ? null : value[key] + '');
|
|
215
|
+
}
|
|
216
|
+
effect(() => update(evaluate(state)))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
196
220
|
let evaluatorMemo = {}
|
|
197
221
|
|
|
198
222
|
// borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
|
|
@@ -214,7 +238,7 @@ function parseExpr(expression, dir, scope) {
|
|
|
214
238
|
// guard static-time eval errors
|
|
215
239
|
let evaluate
|
|
216
240
|
try {
|
|
217
|
-
evaluate = new Function(
|
|
241
|
+
evaluate = new Function( `let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`)
|
|
218
242
|
} catch ( e ) {
|
|
219
243
|
return exprError(e, expression, dir, scope)
|
|
220
244
|
}
|
package/src/index.js
CHANGED
package/test/index.html
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"preact": "../node_modules/preact/dist/preact.mjs",
|
|
20
20
|
"preact/hooks": "../node_modules/preact/hooks/dist/hooks.mjs",
|
|
21
21
|
"@preact/signals-core": "../node_modules/@preact/signals-core/dist/signals-core.mjs",
|
|
22
|
-
"usignal": "../node_modules/usignal/esm/sync.js"
|
|
22
|
+
"usignal/sync": "../node_modules/usignal/esm/sync.js"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
</script>
|
package/test/test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// import { signal } from 'usignal'
|
|
1
|
+
// import { signal } from 'usignal/sync'
|
|
2
2
|
import { signal } from '@preact/signals-core'
|
|
3
3
|
import test, {is, any, throws} from 'tst'
|
|
4
4
|
import {tick, time} from 'wait-please'
|
|
@@ -63,7 +63,7 @@ test('common: class', async () => {
|
|
|
63
63
|
is(el.outerHTML, `<x class="x"></x><y class="y w"></y><z class="b c"></z>`);
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
test('
|
|
66
|
+
test('spraed props: base', async () => {
|
|
67
67
|
let el = h`<input :id="0" :="{for:1, title:2, help:3, type:4, placeholder: 5, value: 6}" :value="7"/>`
|
|
68
68
|
let params = sprae(el)
|
|
69
69
|
is(el.outerHTML, `<input id="0" for="1" title="2" help="3" type="4" placeholder="5" value="7">`)
|
|
@@ -171,7 +171,7 @@ test('conditions: reactive values', async () => {
|
|
|
171
171
|
})
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
test('each:
|
|
174
|
+
test('each: array', async () => {
|
|
175
175
|
// FIXME: in some conspicuous reason jsdom fails to update text nodes somehow
|
|
176
176
|
let el = h`<p>
|
|
177
177
|
<span :each="a in b" :text="a"></span>
|
|
@@ -189,6 +189,24 @@ test('each: base', async () => {
|
|
|
189
189
|
is(el.innerHTML, '')
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
test('each: object', async () => {
|
|
193
|
+
// FIXME: in some conspicuous reason jsdom fails to update text nodes somehow
|
|
194
|
+
let el = h`<p>
|
|
195
|
+
<span :each="x,key in b" :text="[key,x]"></span>
|
|
196
|
+
</p>`
|
|
197
|
+
|
|
198
|
+
const params = sprae(el, { b: null })
|
|
199
|
+
|
|
200
|
+
is(el.innerHTML, '')
|
|
201
|
+
console.log('set 1,2')
|
|
202
|
+
params.b = { x:1, y:2 }
|
|
203
|
+
is(el.innerHTML, '<span>x,1</span><span>y,2</span>')
|
|
204
|
+
params.b = []
|
|
205
|
+
is(el.innerHTML, '')
|
|
206
|
+
params.b = null
|
|
207
|
+
is(el.innerHTML, '')
|
|
208
|
+
})
|
|
209
|
+
|
|
192
210
|
test('each: loop within loop', async () => {
|
|
193
211
|
let el = h`<p>
|
|
194
212
|
<x :each="b in c"><y :each="a in b" :text="a"></y></x>
|
|
@@ -197,7 +215,6 @@ test('each: loop within loop', async () => {
|
|
|
197
215
|
const params = sprae(el, { c: [[1,2], [3,4]] })
|
|
198
216
|
|
|
199
217
|
is(el.innerHTML, '<x><y>1</y><y>2</y></x><x><y>3</y><y>4</y></x>')
|
|
200
|
-
// console.log('set 1,2')
|
|
201
218
|
params.c = [[5,6], [3,4]]
|
|
202
219
|
is(el.innerHTML, '<x><y>5</y><y>6</y></x><x><y>3</y><y>4</y></x>')
|
|
203
220
|
// params.c[1] = [7,8]
|
|
@@ -228,6 +245,9 @@ test('each: reactive values', async () => {
|
|
|
228
245
|
})
|
|
229
246
|
|
|
230
247
|
test('each: loop with condition', async () => {
|
|
248
|
+
// NOTE: there doesn't seem to be much value in exactly that
|
|
249
|
+
// also it creates confusion with :else directive
|
|
250
|
+
// prohibitin that allows in-order directives init
|
|
231
251
|
let el = h`<p>
|
|
232
252
|
<span :each="a in b" :text="a" :if="c"></span>
|
|
233
253
|
</p>`
|
|
@@ -368,7 +388,23 @@ test('with: writes to state', () => {
|
|
|
368
388
|
is(a.innerHTML, `<y>3</y>`)
|
|
369
389
|
})
|
|
370
390
|
|
|
371
|
-
test('
|
|
391
|
+
test('ref: base', () => {
|
|
392
|
+
let a = h`<a :ref="a" :init="log.push(a), null" :text="b"></a>`
|
|
393
|
+
let state = sprae(a, {log:[], b:1})
|
|
394
|
+
is(state.log[0], a)
|
|
395
|
+
is(a.outerHTML, `<a>1</a>`)
|
|
396
|
+
state.b = 2
|
|
397
|
+
is(a.outerHTML, `<a>2</a>`)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('ref with each', () => {
|
|
401
|
+
let a = h`<y><x :ref="x" :each="item in items" :text="log.push(x), item"/></y>`
|
|
402
|
+
let state = sprae(a, {log: [], items: [1,2]})
|
|
403
|
+
is(a.innerHTML, `<x>1</x><x>2</x>`)
|
|
404
|
+
is(state.log, [...a.children])
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('reactive values', async () => {
|
|
372
408
|
let a = new Promise((ok) => setTimeout(() => ok(2), 10))
|
|
373
409
|
|
|
374
410
|
let el = h`<x :text="a">1</x>`
|
|
@@ -377,4 +413,10 @@ test('reactives', async () => {
|
|
|
377
413
|
|
|
378
414
|
await time(20)
|
|
379
415
|
is(el.outerHTML, `<x>2</x>`)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('scope directives must come first', async () => {
|
|
419
|
+
let a = h`<x :text="y" :with="{y:1}" :ref="x"></x>`
|
|
420
|
+
sprae(a, {})
|
|
421
|
+
is(a.outerHTML, `<x>1</x>`)
|
|
380
422
|
})
|
package/src/directives/each.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import sprae, { directive, exprError, parseExpr } from '../core.js'
|
|
2
|
-
|
|
3
|
-
directive(':each', (el, expr) => {
|
|
4
|
-
let each = parseForExpression(expr);
|
|
5
|
-
if (!each) return exprError(new Error, expr);
|
|
6
|
-
|
|
7
|
-
const getItems = parseExpr(each.items);
|
|
8
|
-
|
|
9
|
-
const holder = new Text
|
|
10
|
-
el.replaceWith(holder)
|
|
11
|
-
|
|
12
|
-
// FIXME: make sure no memory leak here
|
|
13
|
-
// FIXME: there can DOM swapper be used instead
|
|
14
|
-
let els = [];
|
|
15
|
-
return state => {
|
|
16
|
-
els.forEach(el => el.remove()); els = [];
|
|
17
|
-
let items = getItems(state);
|
|
18
|
-
if (typeof items === 'number') items = Array.from({length: items}, (item, i)=>i+1)
|
|
19
|
-
items?.forEach((item,i) => {
|
|
20
|
-
const scope = {...state};
|
|
21
|
-
scope[each.item] = item;
|
|
22
|
-
if (each.index) scope[each.index] = i;
|
|
23
|
-
let itemEl = el.cloneNode(true);
|
|
24
|
-
els.push(itemEl);
|
|
25
|
-
holder.before(itemEl);
|
|
26
|
-
sprae(itemEl, scope);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
|
|
32
|
-
function parseForExpression(expression) {
|
|
33
|
-
let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
|
34
|
-
let stripParensRE = /^\s*\(|\)\s*$/g
|
|
35
|
-
let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
|
|
36
|
-
let inMatch = expression.match(forAliasRE)
|
|
37
|
-
|
|
38
|
-
if (!inMatch) return
|
|
39
|
-
|
|
40
|
-
let res = {}
|
|
41
|
-
res.items = inMatch[2].trim()
|
|
42
|
-
let item = inMatch[1].replace(stripParensRE, '').trim()
|
|
43
|
-
let iteratorMatch = item.match(forIteratorRE)
|
|
44
|
-
|
|
45
|
-
if (iteratorMatch) {
|
|
46
|
-
res.item = item.replace(forIteratorRE, '').trim()
|
|
47
|
-
res.index = iteratorMatch[1].trim()
|
|
48
|
-
} else {
|
|
49
|
-
res.item = item
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return res
|
|
53
|
-
}
|