zero-query 1.0.9 → 1.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/cli/commands/build-api.js +442 -0
- package/cli/commands/build.js +33 -2
- package/cli/commands/bundle.js +41 -0
- package/cli/commands/dev/server.js +56 -3
- package/cli/scaffold/default/app/components/contacts/contacts.css +9 -9
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +5 -5
- package/cli/scaffold/default/app/components/playground/playground.js +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +3 -3
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +4 -4
- package/cli/utils.js +6 -6
- package/dist/API.md +6603 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +387 -25
- package/dist/zquery.min.js +47 -17
- package/index.d.ts +9 -3
- package/index.js +10 -2
- package/package.json +2 -1
- package/src/component.js +243 -6
- package/src/reactive.js +4 -3
- package/src/router.js +79 -9
- package/src/store.js +49 -3
- package/tests/cli.test.js +80 -0
- package/tests/compare.test.js +486 -0
- package/tests/dev-server.test.js +489 -0
- package/tests/docs.test.js +1650 -0
- package/tests/electron-features.test.js +864 -0
- package/types/misc.d.ts +7 -7
- package/types/reactive.d.ts +1 -1
- package/types/store.d.ts +2 -1
package/dist/zquery.min.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v1.
|
|
2
|
+
* zQuery (zeroQuery) v1.1.1
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -37,10 +37,11 @@ Signal._activeEffect=execute;try{fn();}
|
|
|
37
37
|
catch(err){reportError(ErrorCode.EFFECT_EXEC,'Effect function threw',{},err);}
|
|
38
38
|
finally{Signal._activeEffect=null;}};execute._deps=new Set();execute();return()=>{if(execute._deps){for(const sig of execute._deps){sig._subscribers.delete(execute);}
|
|
39
39
|
execute._deps.clear();}};}
|
|
40
|
-
function batch(fn){if(Signal._batching){fn();
|
|
41
|
-
Signal._batching=true;Signal._batchQueue.clear();try{fn();}finally{Signal._batching=false;const subs=new Set();for(const sig of Signal._batchQueue){for(const sub of sig._subscribers){subs.add(sub);}}
|
|
40
|
+
function batch(fn){if(Signal._batching){return fn();}
|
|
41
|
+
Signal._batching=true;Signal._batchQueue.clear();let result;try{result=fn();}finally{Signal._batching=false;const subs=new Set();for(const sig of Signal._batchQueue){for(const sub of sig._subscribers){subs.add(sub);}}
|
|
42
42
|
Signal._batchQueue.clear();for(const sub of subs){try{sub();}
|
|
43
|
-
catch(err){reportError(ErrorCode.SIGNAL_CALLBACK,'Signal subscriber threw',{},err);}}}
|
|
43
|
+
catch(err){reportError(ErrorCode.SIGNAL_CALLBACK,'Signal subscriber threw',{},err);}}}
|
|
44
|
+
return result;}
|
|
44
45
|
function untracked(fn){const prev=Signal._activeEffect;Signal._activeEffect=null;try{return fn();}finally{Signal._activeEffect=prev;}}
|
|
45
46
|
let _tpl=null;function _getTemplate(){if(!_tpl)_tpl=document.createElement('template');return _tpl;}
|
|
46
47
|
function morph(rootEl,newHTML){const start=typeof window!=='undefined'&&window.__zqMorphHook?performance.now():0;const tpl=_getTemplate();tpl.innerHTML=newHTML;const newRoot=tpl.content;const tempDiv=document.createElement('div');while(newRoot.firstChild)tempDiv.appendChild(newRoot.firstChild);_morphChildren(rootEl,tempDiv);if(start)window.__zqMorphHook(rootEl,performance.now()-start);}
|
|
@@ -329,13 +330,23 @@ return undefined;}
|
|
|
329
330
|
function _getPath(obj,path){return path.split('.').reduce((o,k)=>o?.[k],obj);}
|
|
330
331
|
function _setPath(obj,path,value){const keys=path.split('.');const last=keys.pop();const target=keys.reduce((o,k)=>(o&&typeof o==='object')?o[k]:undefined,obj);if(target&&typeof target==='object')target[last]=value;}
|
|
331
332
|
class Component{constructor(el,definition,props={}){this._uid=++_uid;this._el=el;this._def=definition;this._mounted=false;this._destroyed=false;this._updateQueued=false;this._listeners=[];this._watchCleanups=[];this.refs={};this._slotContent={};const defaultSlotNodes=[];[...el.childNodes].forEach(node=>{if(node.nodeType===1&&node.hasAttribute('slot')){const slotName=node.getAttribute('slot')||'default';if(!this._slotContent[slotName])this._slotContent[slotName]='';this._slotContent[slotName]+=node.outerHTML;}else if(node.nodeType===1||(node.nodeType===3&&node.textContent.trim())){defaultSlotNodes.push(node.nodeType===1?node.outerHTML:node.textContent);}});if(defaultSlotNodes.length){this._slotContent['default']=defaultSlotNodes.join('');}
|
|
332
|
-
|
|
333
|
+
if(definition.props&&typeof definition.props==='object'&&!Array.isArray(definition.props)){this.props=this._resolveReactiveProps(definition.props,props);this._propObserver=new MutationObserver((mutations)=>{if(this._destroyed)return;let changed=false;for(const mut of mutations){if(mut.type==='attributes'){const attrName=mut.attributeName;if(attrName.startsWith('z-')||attrName.startsWith('@')||attrName.startsWith(':')||attrName.startsWith('data-zq'))continue;const propName=attrName.startsWith(':')?attrName.slice(1):attrName;if(propName in definition.props){changed=true;}}}
|
|
334
|
+
if(changed){this.props=this._resolveReactiveProps(definition.props,{});this._scheduleUpdate();}});this._propObserver.observe(el,{attributes:true});}else{this.props=Object.freeze({...props});}
|
|
335
|
+
this._storeCleanups=[];this.stores={};if(definition.stores&&typeof definition.stores==='object'){for(const[alias,connector]of Object.entries(definition.stores)){if(!connector||!connector._zqConnector)continue;const{store,keys}=connector;const snap={};for(const key of keys){snap[key]=store.state[key];}
|
|
336
|
+
this.stores[alias]=snap;const unsub=store.subscribe(keys,(key,value)=>{this.stores[alias][key]=value;if(!this._destroyed)this._scheduleUpdate();});this._storeCleanups.push(unsub);}}
|
|
337
|
+
const initialState=typeof definition.state==='function'?definition.state():{...(definition.state||{})};this.state=reactive(initialState,(key,value,old)=>{if(!this._destroyed){this._runWatchers(key,value,old);this._scheduleUpdate();}});this.computed={};if(definition.computed){for(const[name,fn]of Object.entries(definition.computed)){Object.defineProperty(this.computed,name,{get:()=>fn.call(this,this.state.__raw||this.state),enumerable:true});}}
|
|
333
338
|
for(const[key,val]of Object.entries(definition)){if(typeof val==='function'&&!_reservedKeys.has(key)){this[key]=val.bind(this);}}
|
|
334
339
|
if(definition.init){try{definition.init.call(this);}
|
|
335
340
|
catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${definition._name}" init() threw`,{component:definition._name},err);}}
|
|
336
341
|
if(definition.watch){this._prevWatchValues={};for(const key of Object.keys(definition.watch)){this._prevWatchValues[key]=_getPath(this.state.__raw||this.state,key);}}}
|
|
337
342
|
_runWatchers(changedKey,value,old){const watchers=this._def.watch;if(!watchers)return;for(const[key,handler]of Object.entries(watchers)){if(changedKey===key||key.startsWith(changedKey+'.')||changedKey.startsWith(key+'.')){const currentVal=_getPath(this.state.__raw||this.state,key);const prevVal=this._prevWatchValues?.[key];if(currentVal!==prevVal){const fn=typeof handler==='function'?handler:handler.handler;if(typeof fn==='function')fn.call(this,currentVal,prevVal);if(this._prevWatchValues)this._prevWatchValues[key]=currentVal;}}}}
|
|
338
343
|
_scheduleUpdate(){if(this._updateQueued)return;this._updateQueued=true;queueMicrotask(()=>{try{if(!this._destroyed)this._render();}finally{this._updateQueued=false;}});}
|
|
344
|
+
_resolveReactiveProps(propDefs,passedProps){const resolved={};for(const[name,schema]of Object.entries(propDefs)){const def=typeof schema==='object'&&schema!==null?schema:{type:schema};const type=def.type;const defaultVal=def.default;if(name in passedProps){resolved[name]=passedProps[name];continue;}
|
|
345
|
+
let rawAttr=this._el.getAttribute(':'+name);let hasAttr=rawAttr!==null;if(!hasAttr){rawAttr=this._el.getAttribute(name);hasAttr=rawAttr!==null;}
|
|
346
|
+
if(hasAttr&&rawAttr!==null){resolved[name]=this._coercePropValue(rawAttr,type);}else if(defaultVal!==undefined){resolved[name]=typeof defaultVal==='function'?defaultVal():defaultVal;}else{resolved[name]=undefined;}}
|
|
347
|
+
return Object.freeze(resolved);}
|
|
348
|
+
_coercePropValue(raw,type){if(type===Number)return Number(raw);if(type===Boolean)return raw!=='false'&&raw!=='0'&&raw!=='';if(type===Object||type===Array){try{return JSON.parse(raw);}catch{return raw;}}
|
|
349
|
+
return raw;}
|
|
339
350
|
async _loadExternals(){const def=this._def;const base=def._base;if(def.templateUrl&&!def._templateLoaded){const tu=def.templateUrl;if(typeof tu==='string'){def._externalTemplate=await _fetchResource(_resolveUrl(tu,base));}else if(Array.isArray(tu)){const urls=tu.map(u=>_resolveUrl(u,base));const results=await Promise.all(urls.map(u=>_fetchResource(u)));def._externalTemplates={};results.forEach((html,i)=>{def._externalTemplates[i]=html;});}else if(typeof tu==='object'){const entries=Object.entries(tu);const results=await Promise.all(entries.map(([,url])=>_fetchResource(_resolveUrl(url,base))));def._externalTemplates={};entries.forEach(([key],i)=>{def._externalTemplates[key]=results[i];});}
|
|
340
351
|
def._templateLoaded=true;}
|
|
341
352
|
if(def.styleUrl&&!def._styleLoaded){const su=def.styleUrl;if(typeof su==='string'){const resolved=_resolveUrl(su,base);def._externalStyles=await _fetchResource(resolved);def._resolvedStyleUrls=[resolved];}else if(Array.isArray(su)){const urls=su.map(u=>_resolveUrl(u,base));const results=await Promise.all(urls.map(u=>_fetchResource(u)));def._externalStyles=results.join('\n');def._resolvedStyleUrls=urls;}
|
|
@@ -389,19 +400,27 @@ parent.replaceChild(fragment,el);}
|
|
|
389
400
|
if(root.querySelector('[z-for]'))_recurse(root);};_recurse(temp);return temp.innerHTML;}
|
|
390
401
|
_expandContentDirectives(html){if(!html.includes('z-html')&&!html.includes('z-text'))return html;const temp=document.createElement('div');temp.innerHTML=html;temp.querySelectorAll('[z-html]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-html'));el.innerHTML=val!=null?String(val):'';el.removeAttribute('z-html');});temp.querySelectorAll('[z-text]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-text'));el.textContent=val!=null?String(val):'';el.removeAttribute('z-text');});return temp.innerHTML;}
|
|
391
402
|
_processDirectives(){const ifEls=[...this._el.querySelectorAll('[z-if]')];for(const el of ifEls){if(!el.parentNode||el.closest('[z-pre]'))continue;const show=!!this._evalExpr(el.getAttribute('z-if'));const chain=[{el,show}];let sib=el.nextElementSibling;while(sib){if(sib.hasAttribute('z-else-if')){chain.push({el:sib,show:!!this._evalExpr(sib.getAttribute('z-else-if'))});sib=sib.nextElementSibling;}else if(sib.hasAttribute('z-else')){chain.push({el:sib,show:true});break;}else{break;}}
|
|
392
|
-
let found=false;for(const item of chain){if(!found&&item.show){found=true;item.el.removeAttribute('z-if');item.el.removeAttribute('z-else-if');item.el.removeAttribute('z-else');}else{item.el.remove();}}}
|
|
393
|
-
this._el.querySelectorAll('[z-show]').forEach(el=>{if(el.closest('[z-pre]'))return;const show=!!this._evalExpr(el.getAttribute('z-show'));el.style.display=show?'':'none';el.removeAttribute('z-show');});const walker=document.createTreeWalker(this._el,NodeFilter.SHOW_ELEMENT,{acceptNode(n){return n.hasAttribute('z-pre')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT;}});let node;while((node=walker.nextNode())){const attrs=node.attributes;for(let i=attrs.length-1;i>=0;i--){const attr=attrs[i];let attrName;if(attr.name.startsWith('z-bind:'))attrName=attr.name.slice(7);else if(attr.name.charCodeAt(0)===58&&attr.name.charCodeAt(1)!==58)attrName=attr.name.slice(1);else continue;const val=this._evalExpr(attr.value);node.removeAttribute(attr.name);if(val===false||val===null||val===undefined){node.removeAttribute(attrName);}else if(val===true){node.setAttribute(attrName,'');}else{node.setAttribute(attrName,String(val));}}}
|
|
403
|
+
let found=false;for(const item of chain){if(!found&&item.show){found=true;item.el.removeAttribute('z-if');item.el.removeAttribute('z-else-if');item.el.removeAttribute('z-else');const transName=item.el.getAttribute('z-transition');if(transName){item.el.removeAttribute('z-transition');this._transitionEnter(item.el,transName);}}else{const transName=item.el.getAttribute('z-transition');if(transName){this._transitionLeave(item.el,transName,()=>item.el.remove());}else{item.el.remove();}}}}
|
|
404
|
+
this._el.querySelectorAll('[z-show]').forEach(el=>{if(el.closest('[z-pre]'))return;const show=!!this._evalExpr(el.getAttribute('z-show'));const transName=el.getAttribute('z-transition');const wasHidden=el.style.display==='none'||el.hasAttribute('data-zq-hidden');if(transName){el.removeAttribute('z-show');if(show&&wasHidden){el.style.display='';el.removeAttribute('data-zq-hidden');this._transitionEnter(el,transName);}else if(!show&&!wasHidden){el.setAttribute('data-zq-hidden','');this._transitionLeave(el,transName,()=>{el.style.display='none';});}else{el.style.display=show?'':'none';if(!show)el.setAttribute('data-zq-hidden','');else el.removeAttribute('data-zq-hidden');}}else{el.style.display=show?'':'none';el.removeAttribute('z-show');}});const walker=document.createTreeWalker(this._el,NodeFilter.SHOW_ELEMENT,{acceptNode(n){return n.hasAttribute('z-pre')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT;}});let node;while((node=walker.nextNode())){const attrs=node.attributes;for(let i=attrs.length-1;i>=0;i--){const attr=attrs[i];let attrName;if(attr.name.startsWith('z-bind:'))attrName=attr.name.slice(7);else if(attr.name.charCodeAt(0)===58&&attr.name.charCodeAt(1)!==58)attrName=attr.name.slice(1);else continue;const val=this._evalExpr(attr.value);node.removeAttribute(attr.name);if(val===false||val===null||val===undefined){node.removeAttribute(attrName);}else if(val===true){node.setAttribute(attrName,'');}else{node.setAttribute(attrName,String(val));}}}
|
|
394
405
|
this._el.querySelectorAll('[z-class]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-class'));if(typeof val==='string'){val.split(/\s+/).filter(Boolean).forEach(c=>el.classList.add(c));}else if(Array.isArray(val)){val.filter(Boolean).forEach(c=>el.classList.add(String(c)));}else if(val&&typeof val==='object'){for(const[cls,active]of Object.entries(val)){el.classList.toggle(cls,!!active);}}
|
|
395
406
|
el.removeAttribute('z-class');});this._el.querySelectorAll('[z-style]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-style'));if(typeof val==='string'){el.style.cssText+=';'+val;}else if(val&&typeof val==='object'){for(const[prop,v]of Object.entries(val)){el.style[prop]=v;}}
|
|
396
407
|
el.removeAttribute('z-style');});this._el.querySelectorAll('[z-cloak]').forEach(el=>{el.removeAttribute('z-cloak');});}
|
|
408
|
+
_transitionEnter(el,name){const cfg=this._def.transition;if(cfg&&cfg.enter){el.classList.add(cfg.enter);const duration=cfg.duration||0;const cleanup=()=>el.classList.remove(cfg.enter);if(duration>0){setTimeout(cleanup,duration);}else{el.addEventListener('transitionend',cleanup,{once:true});el.addEventListener('animationend',cleanup,{once:true});}
|
|
409
|
+
return;}
|
|
410
|
+
el.classList.add(`${name}-enter-from`,`${name}-enter-active`);void el.offsetHeight;requestAnimationFrame(()=>{el.classList.remove(`${name}-enter-from`);el.classList.add(`${name}-enter-to`);const onEnd=()=>{el.classList.remove(`${name}-enter-active`,`${name}-enter-to`);};el.addEventListener('transitionend',onEnd,{once:true});el.addEventListener('animationend',onEnd,{once:true});});}
|
|
411
|
+
_transitionLeave(el,name,done){const cfg=this._def.transition;if(cfg&&cfg.leave){el.classList.add(cfg.leave);const duration=cfg.duration||0;const cleanup=()=>{el.classList.remove(cfg.leave);done();};if(duration>0){setTimeout(cleanup,duration);}else{el.addEventListener('transitionend',cleanup,{once:true});el.addEventListener('animationend',cleanup,{once:true});}
|
|
412
|
+
return;}
|
|
413
|
+
el.classList.add(`${name}-leave-from`,`${name}-leave-active`);void el.offsetHeight;requestAnimationFrame(()=>{el.classList.remove(`${name}-leave-from`);el.classList.add(`${name}-leave-to`);const onEnd=()=>{el.classList.remove(`${name}-leave-active`,`${name}-leave-to`);done();};el.addEventListener('transitionend',onEnd,{once:true});el.addEventListener('animationend',onEnd,{once:true});});}
|
|
397
414
|
setState(partial){if(partial&&Object.keys(partial).length>0){Object.assign(this.state,partial);}else{this._scheduleUpdate();}}
|
|
398
415
|
emit(name,detail){this._el.dispatchEvent(new CustomEvent(name,{detail,bubbles:true,cancelable:true}));}
|
|
399
416
|
destroy(){if(this._destroyed)return;this._destroyed=true;if(this._def.destroyed){try{this._def.destroyed.call(this);}
|
|
400
417
|
catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${this._def._name}" destroyed() threw`,{component:this._def._name},err);}}
|
|
418
|
+
if(this._propObserver){this._propObserver.disconnect();this._propObserver=null;}
|
|
419
|
+
if(this._storeCleanups){this._storeCleanups.forEach(fn=>fn());this._storeCleanups=[];}
|
|
401
420
|
this._listeners.forEach(({event,handler})=>this._el.removeEventListener(event,handler));this._listeners=[];if(this._outsideListeners){this._outsideListeners.forEach(({event,handler})=>document.removeEventListener(event,handler,true));this._outsideListeners=[];}
|
|
402
421
|
this._delegatedEvents=null;this._eventBindings=null;const allEls=this._el.querySelectorAll('*');allEls.forEach(child=>{const dTimers=_debounceTimers.get(child);if(dTimers){for(const key in dTimers)clearTimeout(dTimers[key]);_debounceTimers.delete(child);}
|
|
403
422
|
const tTimers=_throttleTimers.get(child);if(tTimers){for(const key in tTimers)clearTimeout(tTimers[key]);_throttleTimers.delete(child);}});if(this._styleEl)this._styleEl.remove();_instances.delete(this._el);this._el.innerHTML='';}}
|
|
404
|
-
const _reservedKeys=new Set(['state','render','styles','init','mounted','updated','destroyed','props','templateUrl','styleUrl','templates','base','computed','watch']);function component(name,definition){if(!name||typeof name!=='string'){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,'Component name must be a non-empty string');}
|
|
423
|
+
const _reservedKeys=new Set(['state','render','styles','init','mounted','updated','destroyed','props','templateUrl','styleUrl','templates','base','computed','watch','stores','transition','activated','deactivated']);function component(name,definition){if(!name||typeof name!=='string'){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,'Component name must be a non-empty string');}
|
|
405
424
|
if(!name.includes('-')){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,`Component name "${name}" must contain a hyphen (Web Component convention)`);}
|
|
406
425
|
definition._name=name;if(definition.base!==undefined){definition._base=definition.base;}else{definition._base=_detectCallerBase();}
|
|
407
426
|
_registry.set(name,definition);}
|
|
@@ -423,7 +442,7 @@ const link=document.createElement('link');link.rel='stylesheet';link.href=url;li
|
|
|
423
442
|
const ready=Promise.all(loadPromises).then(()=>{if(_criticalStyle){_criticalStyle.remove();}});return{ready,remove(){for(const el of elements){el.remove();for(const[k,v]of _globalStyles){if(v===el){_globalStyles.delete(k);break;}}}}};}
|
|
424
443
|
const _ZQ_STATE_KEY='__zq';function _shallowEqual(a,b){if(a===b)return true;if(!a||!b)return false;const keysA=Object.keys(a);const keysB=Object.keys(b);if(keysA.length!==keysB.length)return false;for(let i=0;i<keysA.length;i++){const k=keysA[i];if(a[k]!==b[k])return false;}
|
|
425
444
|
return true;}
|
|
426
|
-
class Router{constructor(config={}){this._el=null;const isFile=typeof location!=='undefined'&&location.protocol==='file:';this._mode=isFile?'hash':(config.mode||'history');let rawBase=config.base;if(rawBase==null){rawBase=(typeof window!=='undefined'&&window.__ZQ_BASE)||'';if(!rawBase&&typeof document!=='undefined'){const baseEl=document.querySelector('base');if(baseEl){try{rawBase=new URL(baseEl.href).pathname;}
|
|
445
|
+
class Router{constructor(config={}){this._el=null;const isFile=typeof location!=='undefined'&&location.protocol==='file:';this._mode=isFile?'hash':(config.mode||'history');this._keepAliveCache=new Map();let rawBase=config.base;if(rawBase==null){rawBase=(typeof window!=='undefined'&&window.__ZQ_BASE)||'';if(!rawBase&&typeof document!=='undefined'){const baseEl=document.querySelector('base');if(baseEl){try{rawBase=new URL(baseEl.href).pathname;}
|
|
427
446
|
catch{rawBase=baseEl.getAttribute('href')||'';}
|
|
428
447
|
if(rawBase==='/')rawBase='';}}}
|
|
429
448
|
this._base=String(rawBase).replace(/\/+$/,'');if(this._base&&!this._base.startsWith('/'))this._base='/'+this._base;this._routes=[];this._fallback=config.fallback||null;this._current=null;this._guards={before:[],after:[]};this._listeners=new Set();this._instance=null;this._resolving=false;this._substateListeners=[];this._inSubstate=false;if(config.el){this._el=typeof config.el==='string'?document.querySelector(config.el):config.el;}else if(typeof document!=='undefined'){const outlet=document.querySelector('z-outlet');if(outlet){this._el=outlet;if(!config.fallback&&outlet.getAttribute('fallback')){this._fallback=outlet.getAttribute('fallback');}
|
|
@@ -481,17 +500,25 @@ return this.__resolve();}}catch(err){reportError(ErrorCode.ROUTER_GUARD,'Before-
|
|
|
481
500
|
if(matched.load){try{await matched.load();}
|
|
482
501
|
catch(err){reportError(ErrorCode.ROUTER_LOAD,`Failed to load module for route "${matched.path}"`,{path:matched.path},err);return;}}
|
|
483
502
|
this._current=to;if(this._el&&matched.component){if(typeof matched.component==='string'){await prefetch(matched.component);}
|
|
484
|
-
if(this._instance){this.
|
|
485
|
-
|
|
486
|
-
if(
|
|
487
|
-
|
|
503
|
+
const isKeepAlive=!!matched.keepAlive;const componentName=typeof matched.component==='string'?matched.component:null;if(this._instance&&this._currentKeepAlive&&this._currentComponentName){const cached=this._keepAliveCache.get(this._currentComponentName);if(cached){cached.container.style.display='none';if(cached.instance._def.deactivated){try{cached.instance._def.deactivated.call(cached.instance);}
|
|
504
|
+
catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${this._currentComponentName}" deactivated() threw`,{component:this._currentComponentName},err);}}}
|
|
505
|
+
this._instance=null;}else if(this._instance){this._instance.destroy();this._instance=null;}
|
|
506
|
+
const _routeStart=typeof window!=='undefined'&&window.__zqRenderHook?performance.now():0;const props={...params,$route:to,$query:query,$params:params};if(isKeepAlive&&componentName&&this._keepAliveCache.has(componentName)){const cached=this._keepAliveCache.get(componentName);[...this._el.children].forEach(c=>{c.style.display='none';});cached.container.style.display='';this._instance=cached.instance;this._currentKeepAlive=true;this._currentComponentName=componentName;if(cached.instance._def.activated){try{cached.instance._def.activated.call(cached.instance);}
|
|
507
|
+
catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${componentName}" activated() threw`,{component:componentName},err);}}
|
|
508
|
+
if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',componentName);}
|
|
509
|
+
else if(componentName){[...this._el.children].forEach(c=>{if(c.dataset.zqKeepAlive){c.style.display='none';}});[...this._el.children].forEach(c=>{if(!c.dataset.zqKeepAlive)c.remove();});const container=document.createElement(componentName);if(isKeepAlive)container.dataset.zqKeepAlive=componentName;this._el.appendChild(container);try{this._instance=mount(container,componentName,props);}catch(err){reportError(ErrorCode.COMP_NOT_FOUND,`Failed to mount component for route "${matched.path}"`,{component:matched.component,path:matched.path},err);return;}
|
|
510
|
+
if(isKeepAlive){this._keepAliveCache.set(componentName,{container,instance:this._instance});if(this._instance._def.activated){try{this._instance._def.activated.call(this._instance);}
|
|
511
|
+
catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${componentName}" activated() threw`,{component:componentName},err);}}}
|
|
512
|
+
this._currentKeepAlive=isKeepAlive;this._currentComponentName=componentName;if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',componentName);}
|
|
513
|
+
else if(typeof matched.component==='function'){[...this._el.children].forEach(c=>{if(c.dataset.zqKeepAlive)c.style.display='none';else c.remove();});const wrapper=document.createElement('div');wrapper.innerHTML=matched.component(to);while(wrapper.firstChild)this._el.appendChild(wrapper.firstChild);this._currentKeepAlive=false;this._currentComponentName=null;if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',to);}}
|
|
488
514
|
this._updateActiveRoutes(path);for(const guard of this._guards.after){await guard(to,from);}
|
|
489
515
|
this._listeners.forEach(fn=>fn(to,from));}
|
|
490
516
|
_updateActiveRoutes(currentPath){if(typeof document==='undefined')return;const els=document.querySelectorAll('[z-active-route]');for(let i=0;i<els.length;i++){const el=els[i];const route=el.getAttribute('z-active-route');const cls=el.getAttribute('z-active-class')||'active';const exact=el.hasAttribute('z-active-exact');const isActive=exact?currentPath===route:(route==='/'?currentPath==='/':currentPath.startsWith(route));el.classList.toggle(cls,isActive);}}
|
|
491
517
|
destroy(){if(this._onNavEvent){window.removeEventListener(this._mode==='hash'?'hashchange':'popstate',this._onNavEvent);this._onNavEvent=null;}
|
|
492
518
|
if(this._onPopState){window.removeEventListener('popstate',this._onPopState);this._onPopState=null;}
|
|
493
519
|
if(this._onLinkClick){document.removeEventListener('click',this._onLinkClick);this._onLinkClick=null;}
|
|
494
|
-
|
|
520
|
+
for(const[,cached]of this._keepAliveCache){cached.instance.destroy();}
|
|
521
|
+
this._keepAliveCache.clear();if(this._instance)this._instance.destroy();this._listeners.clear();this._substateListeners=[];this._inSubstate=false;this._routes=[];this._guards={before:[],after:[]};}}
|
|
495
522
|
function compilePath(path){const keys=[];const pattern=path.replace(/:(\w+)/g,(_,key)=>{keys.push(key);return'([^/]+)';}).replace(/\*/g,'(.*)');return{regex:new RegExp(`^${pattern}$`),keys};}
|
|
496
523
|
function matchRoute(routes,pathname,fallback='not-found'){for(const route of routes){const{regex,keys}=compilePath(route.path);const m=pathname.match(regex);if(m){const params={};keys.forEach((key,i)=>{params[key]=m[i+1];});return{component:route.component,params};}
|
|
497
524
|
if(route.fallback){const fb=compilePath(route.fallback);const fbm=pathname.match(fb.regex);if(fbm){const params={};fb.keys.forEach((key,i)=>{params[key]=fbm[i+1];});return{component:route.component,params};}}}
|
|
@@ -503,8 +530,9 @@ this._notifySubscribers(key,value,old);});this.getters={};for(const[name,fn]of O
|
|
|
503
530
|
_notifySubscribers(key,value,old){const subs=this._subscribers.get(key);if(subs)subs.forEach(fn=>{try{fn(key,value,old);}
|
|
504
531
|
catch(err){reportError(ErrorCode.STORE_SUBSCRIBE,`Subscriber for "${key}" threw`,{key},err);}});this._wildcards.forEach(fn=>{try{fn(key,value,old);}
|
|
505
532
|
catch(err){reportError(ErrorCode.STORE_SUBSCRIBE,'Wildcard subscriber threw',{key},err);}});}
|
|
506
|
-
batch(fn){this._batching=true;this._batchQueue=[];try{fn(this.state);}finally{this._batching=false;const last=new Map();for(const entry of this._batchQueue){last.set(entry.key,entry);}
|
|
507
|
-
this._batchQueue=[];for(const{key,value,old}of last.values()){this._notifySubscribers(key,value,old);}}
|
|
533
|
+
batch(fn){this._batching=true;this._batchQueue=[];let result;try{result=fn(this.state);}finally{this._batching=false;const last=new Map();for(const entry of this._batchQueue){last.set(entry.key,entry);}
|
|
534
|
+
this._batchQueue=[];for(const{key,value,old}of last.values()){this._notifySubscribers(key,value,old);}}
|
|
535
|
+
return result;}
|
|
508
536
|
checkpoint(){const snap=JSON.parse(JSON.stringify(this.state.__raw||this.state));this._undoStack.push(snap);if(this._undoStack.length>this._maxUndo){this._undoStack.splice(0,this._undoStack.length-this._maxUndo);}
|
|
509
537
|
this._redoStack=[];}
|
|
510
538
|
undo(){if(this._undoStack.length===0)return false;const current=JSON.parse(JSON.stringify(this.state.__raw||this.state));this._redoStack.push(current);const prev=this._undoStack.pop();this.replaceState(prev);return true;}
|
|
@@ -517,6 +545,7 @@ if(this._debug){console.log(`%c[Store] ${name}`,'color: #4CAF50; font-weight: bo
|
|
|
517
545
|
try{const result=action(this.state,...args);this._history.push({action:name,args,timestamp:Date.now()});if(this._history.length>this._maxHistory){this._history.splice(0,this._history.length-this._maxHistory);}
|
|
518
546
|
return result;}catch(err){reportError(ErrorCode.STORE_ACTION,`Action "${name}" threw`,{action:name,args},err);}}
|
|
519
547
|
subscribe(keyOrFn,fn){if(typeof keyOrFn==='function'){this._wildcards.add(keyOrFn);return()=>this._wildcards.delete(keyOrFn);}
|
|
548
|
+
if(Array.isArray(keyOrFn)){const keys=keyOrFn;const handler=(key,value,old)=>{if(keys.includes(key))fn(key,value,old);};this._wildcards.add(handler);return()=>this._wildcards.delete(handler);}
|
|
520
549
|
if(!this._subscribers.has(keyOrFn)){this._subscribers.set(keyOrFn,new Set());}
|
|
521
550
|
this._subscribers.get(keyOrFn).add(fn);return()=>this._subscribers.get(keyOrFn)?.delete(fn);}
|
|
522
551
|
snapshot(){return JSON.parse(JSON.stringify(this.state.__raw||this.state));}
|
|
@@ -528,6 +557,7 @@ reset(initialState){this.replaceState(initialState||JSON.parse(JSON.stringify(th
|
|
|
528
557
|
let _stores=new Map();function createStore(name,config){if(typeof name==='object'){config=name;name='default';}
|
|
529
558
|
const store=new Store(config);_stores.set(name,store);return store;}
|
|
530
559
|
function getStore(name='default'){return _stores.get(name)||null;}
|
|
560
|
+
function connectStore(store,keys){return{_zqConnector:true,store,keys};}
|
|
531
561
|
const _config={baseURL:'',headers:{'Content-Type':'application/json'},timeout:30000,};const _interceptors={request:[],response:[],};async function request(method,url,data,options={}){if(!url||typeof url!=='string'){throw new Error(`HTTP request requires a URL string, got ${typeof url}`);}
|
|
532
562
|
let fullURL=url.startsWith('http')?url:_config.baseURL+url;let headers={..._config.headers,...options.headers};let body=undefined;const fetchOpts={method:method.toUpperCase(),headers,...options,};if(data!==undefined&&method!=='GET'&&method!=='HEAD'){if(data instanceof FormData){body=data;delete fetchOpts.headers['Content-Type'];}else if(typeof data==='object'){body=JSON.stringify(data);}else{body=data;}
|
|
533
563
|
fetchOpts.body=body;}
|
|
@@ -601,6 +631,6 @@ function retry(fn,opts={}){const{attempts=3,delay=1000,backoff=1}=opts;return ne
|
|
|
601
631
|
function timeout(promise,ms,message){let timer;const race=Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(message||`Timed out after ${ms}ms`)),ms);})]);return race.finally(()=>clearTimeout(timer));}
|
|
602
632
|
function $(selector,context){if(typeof selector==='function'){query.ready(selector);return;}
|
|
603
633
|
return query(selector,context);}
|
|
604
|
-
$.id=query.id;$.class=query.class;$.classes=query.classes;$.tag=query.tag;Object.defineProperty($,'name',{value:query.name,writable:true,configurable:true});$.children=query.children;$.qs=query.qs;$.qsa=query.qsa;$.all=function(selector,context){return queryAll(selector,context);};$.create=query.create;$.ready=query.ready;$.on=query.on;$.off=query.off;$.fn=query.fn;$.reactive=reactive;$.Signal=Signal;$.signal=signal;$.computed=computed;$.effect=effect;$.batch=batch;$.untracked=untracked;$.component=component;$.mount=mount;$.mountAll=mountAll;$.getInstance=getInstance;$.destroy=destroy;$.components=getRegistry;$.prefetch=prefetch;$.style=style;$.morph=morph;$.morphElement=morphElement;$.safeEval=safeEval;$.router=createRouter;$.getRouter=getRouter;$.matchRoute=matchRoute;$.store=createStore;$.getStore=getStore;$.http=http;$.get=http.get;$.post=http.post;$.put=http.put;$.patch=http.patch;$.delete=http.delete;$.head=http.head;$.debounce=debounce;$.throttle=throttle;$.pipe=pipe;$.once=once;$.sleep=sleep;$.escapeHtml=escapeHtml;$.stripHtml=stripHtml;$.html=html;$.trust=trust;$.TrustedHTML=TrustedHTML;$.uuid=uuid;$.camelCase=camelCase;$.kebabCase=kebabCase;$.deepClone=deepClone;$.deepMerge=deepMerge;$.isEqual=isEqual;$.param=param;$.parseQuery=parseQuery;$.storage=storage;$.session=session;$.EventBus=EventBus;$.bus=bus;$.range=range;$.unique=unique;$.chunk=chunk;$.groupBy=groupBy;$.pick=pick;$.omit=omit;$.getPath=getPath;$.setPath=setPath;$.isEmpty=isEmpty;$.capitalize=capitalize;$.truncate=truncate;$.clamp=clamp;$.memoize=memoize;$.retry=retry;$.timeout=timeout;$.onError=onError;$.ZQueryError=ZQueryError;$.ErrorCode=ErrorCode;$.guardCallback=guardCallback;$.guardAsync=guardAsync;$.validate=validate;$.formatError=formatError;$.version='1.
|
|
634
|
+
$.id=query.id;$.class=query.class;$.classes=query.classes;$.tag=query.tag;Object.defineProperty($,'name',{value:query.name,writable:true,configurable:true});$.children=query.children;$.qs=query.qs;$.qsa=query.qsa;$.all=function(selector,context){return queryAll(selector,context);};$.create=query.create;$.ready=query.ready;$.on=query.on;$.off=query.off;$.fn=query.fn;$.reactive=reactive;$.Signal=Signal;$.signal=signal;$.computed=computed;$.effect=effect;$.batch=batch;$.untracked=untracked;$.component=component;$.mount=mount;$.mountAll=mountAll;$.getInstance=getInstance;$.destroy=destroy;$.components=getRegistry;$.prefetch=prefetch;$.style=style;$.morph=morph;$.morphElement=morphElement;$.safeEval=safeEval;$.router=createRouter;$.getRouter=getRouter;$.matchRoute=matchRoute;$.store=createStore;$.getStore=getStore;$.connectStore=connectStore;$.http=http;$.get=http.get;$.post=http.post;$.put=http.put;$.patch=http.patch;$.delete=http.delete;$.head=http.head;$.debounce=debounce;$.throttle=throttle;$.pipe=pipe;$.once=once;$.sleep=sleep;$.escapeHtml=escapeHtml;$.stripHtml=stripHtml;$.html=html;$.trust=trust;$.TrustedHTML=TrustedHTML;$.uuid=uuid;$.camelCase=camelCase;$.kebabCase=kebabCase;$.deepClone=deepClone;$.deepMerge=deepMerge;$.isEqual=isEqual;$.param=param;$.parseQuery=parseQuery;$.storage=storage;$.session=session;$.EventBus=EventBus;$.bus=bus;$.range=range;$.unique=unique;$.chunk=chunk;$.groupBy=groupBy;$.pick=pick;$.omit=omit;$.getPath=getPath;$.setPath=setPath;$.isEmpty=isEmpty;$.capitalize=capitalize;$.truncate=truncate;$.clamp=clamp;$.memoize=memoize;$.retry=retry;$.timeout=timeout;$.onError=onError;$.ZQueryError=ZQueryError;$.ErrorCode=ErrorCode;$.guardCallback=guardCallback;$.guardAsync=guardAsync;$.validate=validate;$.formatError=formatError;$.version='1.1.1';$.libSize='~115 KB';$.unitTests={"passed":2281,"failed":0,"total":2281,"suites":565,"duration":6929,"ok":true};$.meta={};$.isElectron=typeof navigator!=='undefined'&&/Electron/i.test(navigator.userAgent)||typeof process!=='undefined'&&process.versions!=null&&!!process.versions.electron;$.platform=$.isElectron?'electron':typeof window!=='undefined'?'browser':'node';$.noConflict=()=>{if(typeof window!=='undefined'&&window.$===$){delete window.$;}
|
|
605
635
|
return $;};if(typeof window!=='undefined'){window.$=$;window.zQuery=$;}
|
|
606
636
|
$;})(typeof window!=='undefined'?window:globalThis);
|
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Lightweight modern frontend library - jQuery-like selectors, reactive
|
|
5
5
|
* components, SPA router, state management, HTTP client & utilities.
|
|
6
6
|
*
|
|
7
|
-
* @version 1.0
|
|
7
|
+
* @version 1.1.0
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @see https://z-query.com/docs
|
|
10
10
|
*/
|
|
@@ -153,7 +153,7 @@ import type { createStore, getStore } from './types/store';
|
|
|
153
153
|
import type { HttpClient } from './types/http';
|
|
154
154
|
import type {
|
|
155
155
|
debounce, throttle, pipe, once, sleep,
|
|
156
|
-
escapeHtml, stripHtml, html, trust, uuid, camelCase, kebabCase,
|
|
156
|
+
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
157
157
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
158
158
|
StorageWrapper, EventBus,
|
|
159
159
|
range, unique, chunk, groupBy,
|
|
@@ -161,7 +161,7 @@ import type {
|
|
|
161
161
|
capitalize, truncate, clamp,
|
|
162
162
|
MemoizedFunction, memoize, RetryOptions, retry, timeout,
|
|
163
163
|
} from './types/utils';
|
|
164
|
-
import type { onError, ZQueryError, ErrorCode, guardCallback, validate } from './types/errors';
|
|
164
|
+
import type { onError, ZQueryError, ErrorCode, guardCallback, guardAsync, validate, formatError } from './types/errors';
|
|
165
165
|
import type { morph, morphElement, safeEval } from './types/misc';
|
|
166
166
|
|
|
167
167
|
/**
|
|
@@ -279,6 +279,7 @@ interface ZQueryStatic {
|
|
|
279
279
|
put: HttpClient['put'];
|
|
280
280
|
patch: HttpClient['patch'];
|
|
281
281
|
delete: HttpClient['delete'];
|
|
282
|
+
head: HttpClient['head'];
|
|
282
283
|
|
|
283
284
|
// -- Error Handling ------------------------------------------------------
|
|
284
285
|
/** Register a global error handler (or pass `null` to remove). */
|
|
@@ -289,8 +290,12 @@ interface ZQueryStatic {
|
|
|
289
290
|
ErrorCode: typeof ErrorCode;
|
|
290
291
|
/** Wrap a callback so thrown errors are caught and reported via the global handler. */
|
|
291
292
|
guardCallback: typeof guardCallback;
|
|
293
|
+
/** Wrap an async function so thrown errors are caught and reported via the global handler. */
|
|
294
|
+
guardAsync: typeof guardAsync;
|
|
292
295
|
/** Validate a required value is defined and of the expected type. */
|
|
293
296
|
validate: typeof validate;
|
|
297
|
+
/** Format a ZQueryError into a structured plain object. */
|
|
298
|
+
formatError: typeof formatError;
|
|
294
299
|
|
|
295
300
|
// -- Utilities -----------------------------------------------------------
|
|
296
301
|
debounce: typeof debounce;
|
|
@@ -303,6 +308,7 @@ interface ZQueryStatic {
|
|
|
303
308
|
stripHtml: typeof stripHtml;
|
|
304
309
|
html: typeof html;
|
|
305
310
|
trust: typeof trust;
|
|
311
|
+
TrustedHTML: typeof TrustedHTML;
|
|
306
312
|
uuid: typeof uuid;
|
|
307
313
|
camelCase: typeof camelCase;
|
|
308
314
|
kebabCase: typeof kebabCase;
|
package/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { query, queryAll, ZQueryCollection } from './src/core.js';
|
|
|
13
13
|
import { reactive, Signal, signal, computed, effect, batch, untracked } from './src/reactive.js';
|
|
14
14
|
import { component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style } from './src/component.js';
|
|
15
15
|
import { createRouter, getRouter, matchRoute } from './src/router.js';
|
|
16
|
-
import { createStore, getStore } from './src/store.js';
|
|
16
|
+
import { createStore, getStore, connectStore } from './src/store.js';
|
|
17
17
|
import { http } from './src/http.js';
|
|
18
18
|
import { morph, morphElement } from './src/diff.js';
|
|
19
19
|
import { safeEval } from './src/expression.js';
|
|
@@ -122,6 +122,7 @@ $.matchRoute = matchRoute;
|
|
|
122
122
|
// --- Store -----------------------------------------------------------------
|
|
123
123
|
$.store = createStore;
|
|
124
124
|
$.getStore = getStore;
|
|
125
|
+
$.connectStore = connectStore;
|
|
125
126
|
|
|
126
127
|
// --- HTTP ------------------------------------------------------------------
|
|
127
128
|
$.http = http;
|
|
@@ -186,6 +187,13 @@ $.libSize = '__LIB_SIZE__';
|
|
|
186
187
|
$.unitTests = '__UNIT_TESTS__';
|
|
187
188
|
$.meta = {}; // populated at build time by CLI bundler
|
|
188
189
|
|
|
190
|
+
// --- Environment detection -------------------------------------------------
|
|
191
|
+
$.isElectron = typeof navigator !== 'undefined' && /Electron/i.test(navigator.userAgent)
|
|
192
|
+
|| typeof process !== 'undefined' && process.versions != null && !!process.versions.electron;
|
|
193
|
+
$.platform = $.isElectron ? 'electron'
|
|
194
|
+
: typeof window !== 'undefined' ? 'browser'
|
|
195
|
+
: 'node';
|
|
196
|
+
|
|
189
197
|
$.noConflict = () => {
|
|
190
198
|
if (typeof window !== 'undefined' && window.$ === $) {
|
|
191
199
|
delete window.$;
|
|
@@ -216,7 +224,7 @@ export {
|
|
|
216
224
|
morph, morphElement,
|
|
217
225
|
safeEval,
|
|
218
226
|
createRouter, getRouter, matchRoute,
|
|
219
|
-
createStore, getStore,
|
|
227
|
+
createStore, getStore, connectStore,
|
|
220
228
|
http,
|
|
221
229
|
ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync, validate, formatError,
|
|
222
230
|
debounce, throttle, pipe, once, sleep,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Lightweight modern frontend library - jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"dev-lib": "node cli/index.js build --watch",
|
|
31
31
|
"bundle": "node cli/index.js bundle",
|
|
32
32
|
"bundle:app": "node cli/index.js bundle zquery-website",
|
|
33
|
+
"build:api": "node cli/commands/build-api.js",
|
|
33
34
|
"test": "vitest run",
|
|
34
35
|
"test:watch": "vitest",
|
|
35
36
|
"test:ssr": "node tests/test-ssr.js"
|
package/src/component.js
CHANGED
|
@@ -210,8 +210,58 @@ class Component {
|
|
|
210
210
|
this._slotContent['default'] = defaultSlotNodes.join('');
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
// Props
|
|
214
|
-
|
|
213
|
+
// Props - reactive when definition.props is defined, frozen otherwise
|
|
214
|
+
if (definition.props && typeof definition.props === 'object' && !Array.isArray(definition.props)) {
|
|
215
|
+
// Reactive props with type coercion and defaults
|
|
216
|
+
this.props = this._resolveReactiveProps(definition.props, props);
|
|
217
|
+
// MutationObserver to re-read props when parent re-renders and changes attributes
|
|
218
|
+
this._propObserver = new MutationObserver((mutations) => {
|
|
219
|
+
if (this._destroyed) return;
|
|
220
|
+
let changed = false;
|
|
221
|
+
for (const mut of mutations) {
|
|
222
|
+
if (mut.type === 'attributes') {
|
|
223
|
+
const attrName = mut.attributeName;
|
|
224
|
+
// Skip internal attributes
|
|
225
|
+
if (attrName.startsWith('z-') || attrName.startsWith('@') || attrName.startsWith(':') || attrName.startsWith('data-zq')) continue;
|
|
226
|
+
// Check if this is a defined prop (attribute names are lowercase)
|
|
227
|
+
const propName = attrName.startsWith(':') ? attrName.slice(1) : attrName;
|
|
228
|
+
if (propName in definition.props) {
|
|
229
|
+
changed = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (changed) {
|
|
234
|
+
this.props = this._resolveReactiveProps(definition.props, {});
|
|
235
|
+
this._scheduleUpdate();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
this._propObserver.observe(el, { attributes: true });
|
|
239
|
+
} else {
|
|
240
|
+
// Legacy: frozen props from parent
|
|
241
|
+
this.props = Object.freeze({ ...props });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Store connectors - auto-subscribe to store keys
|
|
245
|
+
this._storeCleanups = [];
|
|
246
|
+
this.stores = {};
|
|
247
|
+
if (definition.stores && typeof definition.stores === 'object') {
|
|
248
|
+
for (const [alias, connector] of Object.entries(definition.stores)) {
|
|
249
|
+
if (!connector || !connector._zqConnector) continue;
|
|
250
|
+
const { store, keys } = connector;
|
|
251
|
+
// Initialize snapshot
|
|
252
|
+
const snap = {};
|
|
253
|
+
for (const key of keys) {
|
|
254
|
+
snap[key] = store.state[key];
|
|
255
|
+
}
|
|
256
|
+
this.stores[alias] = snap;
|
|
257
|
+
// Subscribe to changes
|
|
258
|
+
const unsub = store.subscribe(keys, (key, value) => {
|
|
259
|
+
this.stores[alias][key] = value;
|
|
260
|
+
if (!this._destroyed) this._scheduleUpdate();
|
|
261
|
+
});
|
|
262
|
+
this._storeCleanups.push(unsub);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
215
265
|
|
|
216
266
|
// Reactive state
|
|
217
267
|
const initialState = typeof definition.state === 'function'
|
|
@@ -290,6 +340,61 @@ class Component {
|
|
|
290
340
|
});
|
|
291
341
|
}
|
|
292
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Resolve reactive props from the definition's prop schema.
|
|
345
|
+
* Reads from element attributes, applies type coercion and defaults.
|
|
346
|
+
* Passed props (from mount) override attributes.
|
|
347
|
+
* @param {object} propDefs - { propName: { type, default } }
|
|
348
|
+
* @param {object} passedProps - props passed programmatically from mount()
|
|
349
|
+
* @returns {object} resolved props (frozen)
|
|
350
|
+
*/
|
|
351
|
+
_resolveReactiveProps(propDefs, passedProps) {
|
|
352
|
+
const resolved = {};
|
|
353
|
+
for (const [name, schema] of Object.entries(propDefs)) {
|
|
354
|
+
const def = typeof schema === 'object' && schema !== null ? schema : { type: schema };
|
|
355
|
+
const type = def.type;
|
|
356
|
+
const defaultVal = def.default;
|
|
357
|
+
|
|
358
|
+
// Priority: passed props > dynamic :prop attribute > static attribute > default
|
|
359
|
+
if (name in passedProps) {
|
|
360
|
+
resolved[name] = passedProps[name];
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check for dynamic :prop attribute (already evaluated by parent mount)
|
|
365
|
+
let rawAttr = this._el.getAttribute(':' + name);
|
|
366
|
+
let hasAttr = rawAttr !== null;
|
|
367
|
+
if (!hasAttr) {
|
|
368
|
+
rawAttr = this._el.getAttribute(name);
|
|
369
|
+
hasAttr = rawAttr !== null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (hasAttr && rawAttr !== null) {
|
|
373
|
+
resolved[name] = this._coercePropValue(rawAttr, type);
|
|
374
|
+
} else if (defaultVal !== undefined) {
|
|
375
|
+
resolved[name] = typeof defaultVal === 'function' ? defaultVal() : defaultVal;
|
|
376
|
+
} else {
|
|
377
|
+
resolved[name] = undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return Object.freeze(resolved);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Coerce a raw attribute string to the specified type.
|
|
385
|
+
* @param {string} raw - attribute value string
|
|
386
|
+
* @param {Function} type - String, Number, Boolean, Object, or Array
|
|
387
|
+
* @returns {*}
|
|
388
|
+
*/
|
|
389
|
+
_coercePropValue(raw, type) {
|
|
390
|
+
if (type === Number) return Number(raw);
|
|
391
|
+
if (type === Boolean) return raw !== 'false' && raw !== '0' && raw !== '';
|
|
392
|
+
if (type === Object || type === Array) {
|
|
393
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
394
|
+
}
|
|
395
|
+
return raw; // String or unspecified
|
|
396
|
+
}
|
|
397
|
+
|
|
293
398
|
// Load external templateUrl / styleUrl if specified (once per definition)
|
|
294
399
|
//
|
|
295
400
|
// Relative paths are resolved automatically against the component file's
|
|
@@ -1052,8 +1157,20 @@ class Component {
|
|
|
1052
1157
|
item.el.removeAttribute('z-if');
|
|
1053
1158
|
item.el.removeAttribute('z-else-if');
|
|
1054
1159
|
item.el.removeAttribute('z-else');
|
|
1160
|
+
// Transition enter for z-if elements becoming visible
|
|
1161
|
+
const transName = item.el.getAttribute('z-transition');
|
|
1162
|
+
if (transName) {
|
|
1163
|
+
item.el.removeAttribute('z-transition');
|
|
1164
|
+
this._transitionEnter(item.el, transName);
|
|
1165
|
+
}
|
|
1055
1166
|
} else {
|
|
1056
|
-
|
|
1167
|
+
// Transition leave for z-if elements being removed
|
|
1168
|
+
const transName = item.el.getAttribute('z-transition');
|
|
1169
|
+
if (transName) {
|
|
1170
|
+
this._transitionLeave(item.el, transName, () => item.el.remove());
|
|
1171
|
+
} else {
|
|
1172
|
+
item.el.remove();
|
|
1173
|
+
}
|
|
1057
1174
|
}
|
|
1058
1175
|
}
|
|
1059
1176
|
}
|
|
@@ -1062,8 +1179,31 @@ class Component {
|
|
|
1062
1179
|
this._el.querySelectorAll('[z-show]').forEach(el => {
|
|
1063
1180
|
if (el.closest('[z-pre]')) return;
|
|
1064
1181
|
const show = !!this._evalExpr(el.getAttribute('z-show'));
|
|
1065
|
-
|
|
1066
|
-
el.
|
|
1182
|
+
const transName = el.getAttribute('z-transition');
|
|
1183
|
+
const wasHidden = el.style.display === 'none' || el.hasAttribute('data-zq-hidden');
|
|
1184
|
+
|
|
1185
|
+
if (transName) {
|
|
1186
|
+
el.removeAttribute('z-show');
|
|
1187
|
+
if (show && wasHidden) {
|
|
1188
|
+
// Entering: was hidden, now showing
|
|
1189
|
+
el.style.display = '';
|
|
1190
|
+
el.removeAttribute('data-zq-hidden');
|
|
1191
|
+
this._transitionEnter(el, transName);
|
|
1192
|
+
} else if (!show && !wasHidden) {
|
|
1193
|
+
// Leaving: was visible, now hiding
|
|
1194
|
+
el.setAttribute('data-zq-hidden', '');
|
|
1195
|
+
this._transitionLeave(el, transName, () => {
|
|
1196
|
+
el.style.display = 'none';
|
|
1197
|
+
});
|
|
1198
|
+
} else {
|
|
1199
|
+
el.style.display = show ? '' : 'none';
|
|
1200
|
+
if (!show) el.setAttribute('data-zq-hidden', '');
|
|
1201
|
+
else el.removeAttribute('data-zq-hidden');
|
|
1202
|
+
}
|
|
1203
|
+
} else {
|
|
1204
|
+
el.style.display = show ? '' : 'none';
|
|
1205
|
+
el.removeAttribute('z-show');
|
|
1206
|
+
}
|
|
1067
1207
|
});
|
|
1068
1208
|
|
|
1069
1209
|
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
@@ -1138,6 +1278,93 @@ class Component {
|
|
|
1138
1278
|
});
|
|
1139
1279
|
}
|
|
1140
1280
|
|
|
1281
|
+
// ---------------------------------------------------------------------------
|
|
1282
|
+
// Transition helpers - CSS class-based enter/leave animations
|
|
1283
|
+
//
|
|
1284
|
+
// z-transition="fade" generates:
|
|
1285
|
+
// Enter: .fade-enter-from → .fade-enter-active + .fade-enter-to
|
|
1286
|
+
// Leave: .fade-leave-from → .fade-leave-active + .fade-leave-to
|
|
1287
|
+
//
|
|
1288
|
+
// Or component-level transition config:
|
|
1289
|
+
// transition: { enter: 'animate-in', leave: 'animate-out', duration: 200 }
|
|
1290
|
+
// ---------------------------------------------------------------------------
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Run an enter transition on an element.
|
|
1294
|
+
* @param {Element} el - target element
|
|
1295
|
+
* @param {string} name - transition name (e.g. 'fade')
|
|
1296
|
+
*/
|
|
1297
|
+
_transitionEnter(el, name) {
|
|
1298
|
+
// Check for component-level transition config
|
|
1299
|
+
const cfg = this._def.transition;
|
|
1300
|
+
if (cfg && cfg.enter) {
|
|
1301
|
+
el.classList.add(cfg.enter);
|
|
1302
|
+
const duration = cfg.duration || 0;
|
|
1303
|
+
const cleanup = () => el.classList.remove(cfg.enter);
|
|
1304
|
+
if (duration > 0) {
|
|
1305
|
+
setTimeout(cleanup, duration);
|
|
1306
|
+
} else {
|
|
1307
|
+
el.addEventListener('transitionend', cleanup, { once: true });
|
|
1308
|
+
el.addEventListener('animationend', cleanup, { once: true });
|
|
1309
|
+
}
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// CSS class-based transition pattern
|
|
1314
|
+
el.classList.add(`${name}-enter-from`, `${name}-enter-active`);
|
|
1315
|
+
// Force reflow so the browser registers the initial state
|
|
1316
|
+
void el.offsetHeight;
|
|
1317
|
+
requestAnimationFrame(() => {
|
|
1318
|
+
el.classList.remove(`${name}-enter-from`);
|
|
1319
|
+
el.classList.add(`${name}-enter-to`);
|
|
1320
|
+
const onEnd = () => {
|
|
1321
|
+
el.classList.remove(`${name}-enter-active`, `${name}-enter-to`);
|
|
1322
|
+
};
|
|
1323
|
+
el.addEventListener('transitionend', onEnd, { once: true });
|
|
1324
|
+
el.addEventListener('animationend', onEnd, { once: true });
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Run a leave transition on an element, then call done().
|
|
1330
|
+
* @param {Element} el - target element
|
|
1331
|
+
* @param {string} name - transition name (e.g. 'fade')
|
|
1332
|
+
* @param {Function} done - callback when transition completes
|
|
1333
|
+
*/
|
|
1334
|
+
_transitionLeave(el, name, done) {
|
|
1335
|
+
// Check for component-level transition config
|
|
1336
|
+
const cfg = this._def.transition;
|
|
1337
|
+
if (cfg && cfg.leave) {
|
|
1338
|
+
el.classList.add(cfg.leave);
|
|
1339
|
+
const duration = cfg.duration || 0;
|
|
1340
|
+
const cleanup = () => {
|
|
1341
|
+
el.classList.remove(cfg.leave);
|
|
1342
|
+
done();
|
|
1343
|
+
};
|
|
1344
|
+
if (duration > 0) {
|
|
1345
|
+
setTimeout(cleanup, duration);
|
|
1346
|
+
} else {
|
|
1347
|
+
el.addEventListener('transitionend', cleanup, { once: true });
|
|
1348
|
+
el.addEventListener('animationend', cleanup, { once: true });
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// CSS class-based transition pattern
|
|
1354
|
+
el.classList.add(`${name}-leave-from`, `${name}-leave-active`);
|
|
1355
|
+
void el.offsetHeight;
|
|
1356
|
+
requestAnimationFrame(() => {
|
|
1357
|
+
el.classList.remove(`${name}-leave-from`);
|
|
1358
|
+
el.classList.add(`${name}-leave-to`);
|
|
1359
|
+
const onEnd = () => {
|
|
1360
|
+
el.classList.remove(`${name}-leave-active`, `${name}-leave-to`);
|
|
1361
|
+
done();
|
|
1362
|
+
};
|
|
1363
|
+
el.addEventListener('transitionend', onEnd, { once: true });
|
|
1364
|
+
el.addEventListener('animationend', onEnd, { once: true });
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1141
1368
|
// Programmatic state update (batch-friendly)
|
|
1142
1369
|
// Passing an empty object forces a re-render (useful for external state changes).
|
|
1143
1370
|
setState(partial) {
|
|
@@ -1161,6 +1388,16 @@ class Component {
|
|
|
1161
1388
|
try { this._def.destroyed.call(this); }
|
|
1162
1389
|
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
|
|
1163
1390
|
}
|
|
1391
|
+
// Clean up prop observer
|
|
1392
|
+
if (this._propObserver) {
|
|
1393
|
+
this._propObserver.disconnect();
|
|
1394
|
+
this._propObserver = null;
|
|
1395
|
+
}
|
|
1396
|
+
// Clean up store connectors
|
|
1397
|
+
if (this._storeCleanups) {
|
|
1398
|
+
this._storeCleanups.forEach(fn => fn());
|
|
1399
|
+
this._storeCleanups = [];
|
|
1400
|
+
}
|
|
1164
1401
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
1165
1402
|
this._listeners = [];
|
|
1166
1403
|
if (this._outsideListeners) {
|
|
@@ -1195,7 +1432,7 @@ class Component {
|
|
|
1195
1432
|
const _reservedKeys = new Set([
|
|
1196
1433
|
'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
|
|
1197
1434
|
'templateUrl', 'styleUrl', 'templates', 'base',
|
|
1198
|
-
'computed', 'watch'
|
|
1435
|
+
'computed', 'watch', 'stores', 'transition', 'activated', 'deactivated'
|
|
1199
1436
|
]);
|
|
1200
1437
|
|
|
1201
1438
|
|
package/src/reactive.js
CHANGED
|
@@ -206,13 +206,13 @@ export function effect(fn) {
|
|
|
206
206
|
export function batch(fn) {
|
|
207
207
|
if (Signal._batching) {
|
|
208
208
|
// Already inside a batch, just run
|
|
209
|
-
fn();
|
|
210
|
-
return;
|
|
209
|
+
return fn();
|
|
211
210
|
}
|
|
212
211
|
Signal._batching = true;
|
|
213
212
|
Signal._batchQueue.clear();
|
|
213
|
+
let result;
|
|
214
214
|
try {
|
|
215
|
-
fn();
|
|
215
|
+
result = fn();
|
|
216
216
|
} finally {
|
|
217
217
|
Signal._batching = false;
|
|
218
218
|
// Collect all unique subscribers across all queued signals
|
|
@@ -231,6 +231,7 @@ export function batch(fn) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
return result;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
|