@elenajs/core 1.0.0-rc.10 → 1.0.0-rc.12

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/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  <br/>
24
24
 
25
- <p align="center">Elena is a simple, tiny library (2.6kB) for building <a href="https://elenajs.com/">Progressive Web Components</a>. Unlike most web component libraries, Elena doesn’t force JavaScript for everything. You can load HTML and CSS first, then use JavaScript to progressively add interactivity.</p>
25
+ <p align="center">Elena is a simple, tiny library for building <a href="https://elenajs.com/">Progressive Web Components</a>. Unlike most web component libraries, Elena doesn’t force JavaScript for everything. You can load HTML and CSS first, then use JavaScript to progressively add interactivity.</p>
26
26
 
27
27
  ## Documentation
28
28
 
package/dist/bundle.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @elenajs/core v1.0.0-rc.10
2
+ * @elenajs/core v1.0.0-rc.12
3
3
  * (c) 2025-present Ariel Salminen and Elena contributors
4
4
  * @license MIT
5
5
  */
6
- const t="░█ [ELENA]: ",s=Array.isArray,i=s=>console.warn(t+s),e={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function n(t){return s(t)?t.map(o).join(""):o(t)}function o(t){return t?.t?String(t):String(t??"").replace(/[&<>"']/g,t=>e[t])}function r(t,...s){let i;return{t:!0,strings:t,values:s,toString:()=>(null==i&&(i=t.reduce((t,i,e)=>t+i+n(s[e]),"")),i)}}function h(t){return{t:!0,toString:()=>t??""}}const c={t:!0,toString:()=>""},u=t=>s(t)?t.some(t=>t?.t):t?.t,f=t=>s(t)?t.join(""):String(t??"");function l(t){return t.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}function a(t,s,e){if(s="boolean"===t&&"boolean"!=typeof s?null!==s:s,!e)return s;if("toAttribute"===e)switch(t){case"object":case"array":return s&&JSON.stringify(s);case"boolean":return s?"":null;default:return""===s?null:s}else switch(t){case"object":case"array":if(!s)return s;try{return JSON.parse(s)}catch{return i("Invalid JSON: "+s),null}case"number":return null!==s?+s:s;default:return s??""}}function d(t,s,e){t?null===e?t.removeAttribute(s):t.setAttribute(s,e):i("Cannot sync attrs.")}const p=new WeakMap,g="e"+(1e5*Math.random()|0),b=()=>document.createElement("template"),y=t=>document.createTreeWalker(t,128);function m(t,i,e){return!function(t,i,e){if(t.i!==i||!t.o)return!1;for(let i=0;i<e.length;i++){const n=e[i],o=s(n)?f(n):n;if(o!==t.h[i]){if(u(n)||!t.o[i])return!1;t.h[i]=o,t.o[i].textContent=f(n)}}return!0}(t,i,e)&&(function(t,i,e){let o=p.get(i);if(!o){const t=i.map(l);o={u:t,l:e.length>0?S(t,e.length):null},p.set(i,o)}if(o.l)t.o=function(t,s,i){const e=s.content.cloneNode(!0),o=y(e),r=Array(i.length),h=[];let c;for(;c=o.nextNode();)c.data===g&&h.push(c);for(let t=0;t<h.length;t++){const s=i[t];if(u(s)){const i=b();i.innerHTML=n(s),h[t].parentNode.replaceChild(i.content,h[t])}else{const i=document.createTextNode(f(s));h[t].parentNode.replaceChild(i,h[t]),r[t]=i}}return t.replaceChildren(e),r}(t,o.l,e);else{const s=e.map(n),i=o.u.reduce((t,i,e)=>t+i+(s[e]??""),"").replace(/>\s+</g,"><").trim(),r=b();r.innerHTML=i,_(t,r.content.childNodes),t.o=null}t.i=i,t.h=e.map(t=>s(t)?f(t):t)}(t,i,e),!0)}function S(t,s){const i=`\x3c!--${g}--\x3e`,e=t.reduce((t,e,n)=>t+e+(n<s?i:""),"").trim(),n=b();n.innerHTML=e;const o=y(n.content);let r=0;for(;o.nextNode();)o.currentNode.data===g&&r++;return r===s?n:null}function _(t,s){const i=Array.from(t.childNodes),e=Array.from(s),n=Math.max(i.length,e.length);for(let s=0;s<n;s++){const n=i[s],o=e[s];n?o?n.nodeType!==o.nodeType||1===n.nodeType&&n.tagName!==o.tagName?t.replaceChild(o,n):3===n.nodeType?n.textContent!==o.textContent&&(n.textContent=o.textContent):1===n.nodeType&&(w(n,o),_(n,o.childNodes)):t.removeChild(n):t.appendChild(o)}}function w(t,s){for(let i=t.attributes.length-1;i>=0;i--){const{name:e}=t.attributes[i];s.hasAttribute(e)||t.removeAttribute(e)}for(let i=0;i<s.attributes.length;i++){const{name:e,value:n}=s.attributes[i];t.getAttribute(e)!==n&&t.setAttribute(e,n)}}const v=new WeakSet,x=(t,s)=>Object.prototype.hasOwnProperty.call(t,s);function C(s){return class extends s{element=null;attributeChangedCallback(t,s,e){super.attributeChangedCallback?.(t,s,e),"text"!==t?(this.p=!0,function(t,s,e,n){if(e!==n){const e=typeof t[s];"undefined"===e&&i(`Prop "${s}" has no default.`);const o=a(e,n,"toProp");t[s]=o}}(this,t,s,e),this.p=!1,this.m&&s!==e&&!this.S&&this._()):this.text=e??""}static get observedAttributes(){if(this.v)return this.v;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this.v=[...t,"text"],this.v}connectedCallback(){super.connectedCallback?.(),this.C(),this.A(),this.m||void 0!==this.k||(this.text=this.textContent.trim()),this.P(),this.willUpdate(),this.M(),this.N(),this.O(),this.m||(this.m=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}C(){const t=this.constructor;if(v.has(t))return;const s=new Set,e=[];if(t.props){for(const i of t.props)"string"==typeof i?e.push(i):(e.push(i.name),!1===i.reflect&&s.add(i.name));e.includes("text")&&i('"text" is reserved.'),function(t,s,i){for(const e of s){const s=!i||!i.has(e);Object.defineProperty(t,e,{configurable:!0,enumerable:!0,get(){return this.j?.get(e)},set(t){if(this.j||(this.j=new Map),t!==this.j.get(e)&&(this.j.set(e,t),this.isConnected))if(s){if(!this.p){const s=a(typeof t,t,"toAttribute");d(this,e,s)}}else this.m&&!this.S&&this._()}})}}(t.prototype,e,s)}if(t.U=e,t.$=s,t.q=t.events||null,t.q)for(const s of t.q)x(t.prototype,s)||(t.prototype[s]=function(...t){return this.element[s](...t)});var n;t.J=(n=t.element)?t=>t.querySelector(n):t=>t.firstElementChild,v.add(t)}A(){this.p=!0;for(const t of this.constructor.U)if(x(this,t)){const s=this[t];delete this[t],this[t]=s}this.p=!1}get R(){return this.W??this.shadowRoot??this}P(){const t=this.constructor;if(!t.shadow)return;this.W||this.shadowRoot||(this.W=this.attachShadow({mode:t.shadow}));const s=this.W??this.shadowRoot;if(t.styles){if(!t.D){const s=[t.styles].flat();t.D=s.map(t=>{if("string"==typeof t){const s=new CSSStyleSheet;return s.replaceSync(t),s}return t})}s.adoptedStyleSheets=t.D}}M(){const t=this.constructor,s=this.R,e=this.render();if(e&&e.strings&&m(s,e.strings,e.values)){const i=this.element;if(this.element=t.J(s),this.F&&i&&this.element!==i){const s=t.q;for(const t of s)i.removeEventListener(t,this),this.element.addEventListener(t,this)}}this.element||(this.element=t.J(s),this.element||(t.element&&i("Element not found."),this.element=s.firstElementChild))}N(){if(this.j){const t=this.constructor.$;for(const[s,i]of this.j){if(t.has(s))continue;const e=a(typeof i,i,"toAttribute");(null!==e||this.hasAttribute(s))&&d(this,s,e)}}}O(){const t=this.constructor.q;if(!this.F&&t?.length)if(this.element){this.F=!0;for(const s of t)this.element.addEventListener(s,this)}else i("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this.F){this.F=!1;for(const t of this.constructor.q)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor.q?.includes(t.type)&&(t.bubbles&&(t.composed||this.R===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this.k??""}set text(t){const s=this.k;this.k=t,this.m&&s!==t&&!this.S&&this._()}static define(){const t=this.tagName;t?function(t,s){const i=globalThis.customElements;i?.get(t)||i?.define(t,s)}(t,this):i("define() without a tagName.")}_(){this.S||this.I||(this.I=!0,this.L=new Promise(t=>{this.T=t}),queueMicrotask(()=>{try{this.B()}catch(s){console.error(t,s)}}))}B(){this.I=!1;const t=this.T;this.T=null;try{try{this.willUpdate(),this.S=!0,this.M()}finally{this.S=!1}this.updated()}finally{this.L=null,t()}}get updateComplete(){return this.L||Promise.resolve()}requestUpdate(){this.m&&!this.S&&this._()}}}export{C as Elena,r as html,c as nothing,h as unsafeHTML};
6
+ const t="░█ [ELENA]: ",s=Array.isArray,i=Symbol("elena.raw"),n=s=>console.warn(t+s),e={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function o(t){return s(t)?t.map(r).join(""):r(t)}function r(t){return t?.[i]?String(t):String(t??"").replace(/[&<>"']/g,t=>e[t])}class h{constructor(t,s){this.strings=t,this.values=s}toString(){return null==this.t&&(this.t=this.strings.reduce((t,s,i)=>t+s+o(this.values[i]),"")),this.t}}function c(t,...s){return new h(t,s)}function l(t){return{[i]:!0,toString:()=>t??""}}h.prototype[i]=!0;const u={[i]:!0,toString:()=>""},f=t=>s(t)?t.some(t=>t?.[i]):!!t?.[i],a=t=>s(t)?t.join(""):String(t??"");function d(t){return t.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}function p(t,s,i){if(s="boolean"===t&&"boolean"!=typeof s?null!==s:s,!i)return s;if("toAttribute"===i)switch(t){case"object":case"array":return s&&JSON.stringify(s);case"boolean":return s?"":null;default:return""===s?null:s}else switch(t){case"object":case"array":if(!s)return s;try{return JSON.parse(s)}catch{return n("Invalid JSON: "+s),null}case"number":return null!==s?+s:s;default:return s??""}}function g(t,s,i){t?null===i?t.removeAttribute(s):t.setAttribute(s,i):n("Cannot sync attrs.")}const y=new WeakMap,b="e"+Math.random().toString(36).slice(2),S=()=>document.createElement("template"),m=t=>document.createTreeWalker(t,128);function _(t,s){const i=`\x3c!--${b}--\x3e`,n=[];let e="";for(let o=0;o<t.length;o++)if(e+=t[o],o<s){const s=t[o].match(/([^\s"'>/=]+)\s*=\s*["']$/);s?(n.push(s[1]),e+=b+"_"+o):(n.push(null),e+=i)}const o=S();o.innerHTML=e.trim();const r=m(o.content);let h=0;for(;r.nextNode();)r.currentNode.data===b&&h++;return h!==n.filter(t=>null===t).length?null:{i:o,o:n}}function w(t,s){const i=Array.from(t.childNodes),n=Array.from(s),e=Math.max(i.length,n.length);for(let s=0;s<e;s++){const e=i[s],o=n[s];e?o?e.nodeType!==o.nodeType||1===e.nodeType&&e.tagName!==o.tagName?t.replaceChild(o,e):3===e.nodeType?e.textContent!==o.textContent&&(e.textContent=o.textContent):1===e.nodeType&&(v(e,o),w(e,o.childNodes)):t.removeChild(e):t.appendChild(o)}}function v(t,s){for(let i=t.attributes.length-1;i>=0;i--){const{name:n}=t.attributes[i];s.hasAttribute(n)||t.removeAttribute(n)}for(let i=0;i<s.attributes.length;i++){const{name:n,value:e}=s.attributes[i];t.getAttribute(n)!==e&&t.setAttribute(n,e)}}const x=new WeakSet,C=(t,s)=>Object.prototype.hasOwnProperty.call(t,s);function A(i){return class extends i{element=null;attributeChangedCallback(t,s,i){if(super.attributeChangedCallback?.(t,s,i),"text"!==t){if(s!==i)if(this.h&&!this.l){const s=this.u.get(t),n=typeof s,e="string"===n?i??"":p(n,i,"toProp");e!==s&&this.u.set(t,e),this.p()}else this.S=!0,function(t,s,i,e){if(i!==e){const i=typeof t[s];"undefined"===i&&n(`Prop "${s}" has no default.`);const o=p(i,e,"toProp");t[s]=o}}(this,t,s,i),this.S=!1}else this.text=i??""}static get observedAttributes(){if(this.m)return this.m;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this.m=[...t,"text"],this.m}connectedCallback(){super.connectedCallback?.(),this._(),this.v(),this.h||void 0!==this.C||(this.text=this.textContent.trim()),this.A(),this.k=this.P??this.shadowRoot??this,this.$??=()=>{try{this.M()}catch(s){console.error(t,s)}},this.willUpdate(),this.N(),this.O(),this.j(),this.h||(this.h=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}_(){const t=this.constructor;if(x.has(t))return;const s=new Set,i=[];if(t.props){for(const n of t.props)"string"==typeof n?i.push(n):(i.push(n.name),!1===n.reflect&&s.add(n.name));i.includes("text")&&n('"text" is reserved.'),function(t,s,i){for(const n of s){const s=!i||!i.has(n);Object.defineProperty(t,n,{configurable:!0,enumerable:!0,get(){return this.u?.get(n)},set(t){if(this.u||(this.u=new Map),t!==this.u.get(n)&&(this.u.set(n,t),this.isConnected))if(s){if(!this.S){const s=p(typeof t,t,"toAttribute");g(this,n,s)}}else this.h&&!this.l&&this.p()}})}}(t.prototype,i,s)}if(t.U=i,t.q=s,t.J=t.events||null,t.J)for(const s of t.J)C(t.prototype,s)||(t.prototype[s]=function(...t){return this.element[s](...t)});var e;t.R=(e=t.element)?t=>t.querySelector(e):t=>t.firstElementChild,x.add(t)}v(){this.S=!0;for(const t of this.constructor.U)if(C(this,t)){const s=this[t];delete this[t],this[t]=s}this.S=!1}A(){const t=this.constructor;if(!t.shadow)return;this.P||this.shadowRoot||(this.P=this.attachShadow({mode:t.shadow}));const s=this.P??this.shadowRoot;if(t.styles){if(!t.W){const s=[t.styles].flat();t.W=s.map(t=>{if("string"==typeof t){const s=new CSSStyleSheet;return s.replaceSync(t),s}return t})}s.adoptedStyleSheets=t.W}}N(){const t=this.constructor,i=this.k,e=this.render();if(e&&e.strings&&!function(t,i,n){if(t.D!==i||!t.F)return!1;const e=t.F,o=t.I;for(let t=0;t<n.length;t++){const i=n[t],r=s(i)?a(i):i;if(r===o[t])continue;if(f(i)&&i!==u)return!1;const h=e[t];if(!h)return!1;o[t]=r;const c=String(r??"");h.nodeType?h.textContent=c:h[0].setAttribute(h[1],c)}return!0}(r=i,h=e.strings,c=e.values)&&(function(t,i,n){let e=y.get(i);if(!e){const t=i.map(d);e={L:t,T:n.length>0?_(t,n.length):null},y.set(i,e)}if(e.T)t.F=function(t,i,n){const{i:e,o:r}=i,h=e.content.cloneNode(!0),c=m(h),l=Array(n.length),d=[];let p;for(;p=c.nextNode();)p.data===b&&d.push(p);let g=0;for(let t=0;t<n.length;t++){const i=r[t];if(i){const e=h.querySelector(`[${i}="${b+"_"+t}"]`);if(e){const o=n[t],r=String((s(o)?a(o):o)??"");e.setAttribute(i,r),l[t]=[e,i]}}else{const s=d[g++],i=n[t];if(f(i)&&i!==u){const t=S();t.innerHTML=o(i),s.parentNode.replaceChild(t.content,s)}else{const n=document.createTextNode(a(i));s.parentNode.replaceChild(n,s),l[t]=n}}}return t.D?(w(t,h.childNodes),null):(t.replaceChildren(h),l)}(t,e.T,n);else{const s=n.map(o),i=e.L.reduce((t,i,n)=>t+i+(s[n]??""),"").replace(/>\s+</g,"><").trim(),r=S();r.innerHTML=i,w(t,r.content.childNodes),t.F=null}t.D=i,t.I=n.map(t=>s(t)?a(t):t)}(r,h,c),1)){const s=this.element;if(this.element=t.R(i),this.B&&s&&this.element!==s){const i=t.J;for(const t of i)s.removeEventListener(t,this),this.element.addEventListener(t,this)}}var r,h,c;this.element||(this.element=t.R(i),this.element||(t.element&&n("Element not found."),this.element=i.firstElementChild))}O(){if(this.u){const t=this.constructor.q;for(const[s,i]of this.u){if(t.has(s))continue;const n=p(typeof i,i,"toAttribute");(null!==n||this.hasAttribute(s))&&g(this,s,n)}}}j(){const t=this.constructor.J;if(!this.B&&t?.length)if(this.element){this.B=!0;for(const s of t)this.element.addEventListener(s,this)}else n("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this.B){this.B=!1;for(const t of this.constructor.J)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor.J?.includes(t.type)&&(t.bubbles&&(t.composed||this.k===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this.C??""}set text(t){const s=this.C;this.C=t,this.h&&s!==t&&!this.l&&this.p()}static define(){const t=this.tagName;t?function(t,s){const i=globalThis.customElements;i?.get(t)||i?.define(t,s)}(t,this):n("define() without a tagName.")}p(){this.l||this.G||(this.G=!0,queueMicrotask(this.$))}M(){this.G=!1;const t=this.H;this.H=null;try{try{this.willUpdate(),this.l=!0,this.N()}finally{this.l=!1}this.updated()}finally{this.K=null,t?.()}}get updateComplete(){return this.G?(this.K||(this.K=new Promise(t=>{this.H=t})),this.K):Promise.resolve()}requestUpdate(){this.h&&!this.l&&this.p()}}}export{A as Elena,c as html,u as nothing,l as unsafeHTML};
@@ -19,11 +19,11 @@ export function syncAttribute(element: Element, name: string, value: string | nu
19
19
  * at class-creation time. Values are stored per-instance
20
20
  * via a `_props` Map that is lazily created.
21
21
  *
22
- * @param {Function} proto - The class prototype
22
+ * @param {Object} proto - The class prototype
23
23
  * @param {string[]} propNames - Prop names to define
24
24
  * @param {Set<string>} [noReflect] - Props that should not reflect to attributes
25
25
  */
26
- export function setProps(proto: Function, propNames: string[], noReflect?: Set<string>): void;
26
+ export function setProps(proto: Object, propNames: string[], noReflect?: Set<string>): void;
27
27
  /**
28
28
  * We need to update the internals of the Elena Element
29
29
  * when props on the host element are changed.
@@ -1 +1 @@
1
- {"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../src/common/props.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCAJW,MAAM,SACN,GAAG,cACH,aAAa,GAAG,QAAQ,OAqClC;AAED;;;;;;GAMG;AACH,uCAJW,OAAO,QACP,MAAM,SACN,MAAM,GAAG,IAAI,QAYvB;AAED;;;;;;;;GAQG;AACH,qDAHW,MAAM,EAAE,cACR,GAAG,CAAC,MAAM,CAAC,QAsCrB;AAED;;;;;;;;GAQG;AACH,kCALW,MAAM,QACN,MAAM,YACN,GAAG,YACH,GAAG,QAWb"}
1
+ {"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../src/common/props.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCAJW,MAAM,SACN,GAAG,cACH,aAAa,GAAG,QAAQ,OAqClC;AAED;;;;;;GAMG;AACH,uCAJW,OAAO,QACP,MAAM,SACN,MAAM,GAAG,IAAI,QAYvB;AAED;;;;;;;;GAQG;AACH,gCAJW,MAAM,aACN,MAAM,EAAE,cACR,GAAG,CAAC,MAAM,CAAC,QAsCrB;AAED;;;;;;;;GAQG;AACH,kCALW,MAAM,QACN,MAAM,YACN,GAAG,YACH,GAAG,QAWb"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Render a tagged template into an Elena Element with DOM diffing.
3
- * Returns true if the DOM was fully rebuilt, false if only text
4
- * nodes were patched in place.
3
+ * Returns true if the DOM was fully rebuilt, false if parts were
4
+ * patched in place.
5
5
  *
6
6
  * @param {HTMLElement} element
7
7
  * @param {TemplateStringsArray} strings - Static parts of the tagged template
@@ -20,10 +20,9 @@ export function resolveValue(value: any): string;
20
20
  *
21
21
  * @param {TemplateStringsArray} strings
22
22
  * @param {...*} values
23
- * @returns {{ __raw: true, strings: TemplateStringsArray, values: Array, toString(): string }}
23
+ * @returns {{ strings: TemplateStringsArray, values: Array, toString(): string }}
24
24
  */
25
25
  export function html(strings: TemplateStringsArray, ...values: any[]): {
26
- __raw: true;
27
26
  strings: TemplateStringsArray;
28
27
  values: any[];
29
28
  toString(): string;
@@ -32,10 +31,9 @@ export function html(strings: TemplateStringsArray, ...values: any[]): {
32
31
  * Renders a string as HTML rather than text.
33
32
  *
34
33
  * @param {string} str - The raw HTML string to trust.
35
- * @returns {{ __raw: true, toString(): string }}
34
+ * @returns {{ toString(): string }}
36
35
  */
37
36
  export function unsafeHTML(str: string): {
38
- __raw: true;
39
37
  toString(): string;
40
38
  };
41
39
  /**
@@ -50,10 +48,9 @@ export function warn(msg: string): void;
50
48
  * A placeholder you can return from a conditional expression
51
49
  * inside a template to render nothing.
52
50
  *
53
- * @type {{ __raw: true, toString(): string }}
51
+ * @type {{ toString(): string }}
54
52
  */
55
53
  export const nothing: {
56
- __raw: true;
57
54
  toString(): string;
58
55
  };
59
56
  export function isRaw(value: any): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/common/utils.js"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,uCAHW,MAAM,2BAMhB;AASD,6CAEC;AAED;;;;;;GAMG;AACH,oCAHW,GAAC,GACC,MAAM,CAOlB;AAaD;;;;;;;GAOG;AACH,8BAJW,oBAAoB,aACjB,GAAC,EAAA,GACF;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAC;IAAC,MAAM,QAAQ;IAAC,QAAQ,IAAI,MAAM,CAAA;CAAE,CAiB7F;AAED;;;;;GAKG;AACH,gCAHW,MAAM,GACJ;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,IAAI,MAAM,CAAA;CAAE,CAI/C;AA0BD;;;;;GAKG;AACH,2CAHW,MAAM,GACJ,MAAM,CAOlB;AAxHM,0BAHI,MAAM,QAGoC;AAqFrD;;;;;GAKG;AACH,sBAFU;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,IAAI,MAAM,CAAA;CAAE,CAEc;AAQpD,6BAHI,GAAC,GACC,OAAO,CAE2E;AAQxF,mCAHI,GAAC,GACC,MAAM,CAEwE;AAlH3F,qBAAe,wBAAc,CAAC;AAC9B,iDAA8B"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/common/utils.js"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,uCAHW,MAAM,2BAMhB;AASD,6CAEC;AAED;;;;;;GAMG;AACH,oCAHW,GAAC,GACC,MAAM,CAOlB;AAkCD;;;;;;;GAOG;AACH,8BAJW,oBAAoB,aACjB,GAAC,EAAA,GACF;IAAE,OAAO,EAAE,oBAAoB,CAAC;IAAC,MAAM,QAAQ;IAAC,QAAQ,IAAI,MAAM,CAAA;CAAE,CAIhF;AAED;;;;;GAKG;AACH,gCAHW,MAAM,GACJ;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAIlC;AA0BD;;;;;GAKG;AACH,2CAHW,MAAM,GACJ,MAAM,CAOlB;AAhIM,0BAHI,MAAM,QAGoC;AA6FrD;;;;;GAKG;AACH,sBAFU;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAE2B;AAQpD,6BAHI,GAAC,GACC,OAAO,CAE6E;AAQ1F,mCAHI,GAAC,GACC,MAAM,CAEwE;AA3H3F,qBAAe,wBAAc,CAAC;AAC9B,iDAA8B"}
package/dist/elena.d.ts CHANGED
@@ -13,12 +13,16 @@ export type ElenaConstructor = new (...args: any[]) => HTMLElement;
13
13
  export type ElenaInstanceMembers = {
14
14
  text: string;
15
15
  element: HTMLElement | null;
16
+ updateComplete: Promise<void>;
16
17
  render(): void;
17
18
  willUpdate(): void;
18
19
  firstUpdated(): void;
19
20
  updated(): void;
21
+ requestUpdate(): void;
20
22
  connectedCallback(): void;
21
23
  disconnectedCallback(): void;
24
+ adoptedCallback(): void;
25
+ attributeChangedCallback(prop: string, oldValue: string | null, newValue: string | null): void;
22
26
  };
23
27
  export type ElenaPropObject = {
24
28
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"elena.d.ts","sourceRoot":"","sources":["../src/elena.js"],"names":[],"mappings":"AAkEA;;;;;;;;;GASG;AACH,kCAHW,gBAAgB,GACd,uBAAuB,CA6dnC;+BAjgBY,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW;mCAInC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAAC,MAAM,IAAI,IAAI,CAAC;IAAC,UAAU,IAAI,IAAI,CAAC;IAAC,YAAY,IAAI,IAAI,CAAC;IAAC,OAAO,IAAI,IAAI,CAAC;IAAC,iBAAiB,IAAI,IAAI,CAAC;IAAC,oBAAoB,IAAI,IAAI,CAAA;CAAE;8BAIjL;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;sCAInC,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW,GAAG,oBAAoB,CAAC,GAAG;IACvE,MAAM,IAAI,IAAI,CAAC;IACnB,QAAY,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;CAC9D;qBA5CmE,mBAAmB;2BAAnB,mBAAmB;wBAAnB,mBAAmB"}
1
+ {"version":3,"file":"elena.d.ts","sourceRoot":"","sources":["../src/elena.js"],"names":[],"mappings":"AAkEA;;;;;;;;;GASG;AACH,kCAHW,gBAAgB,GACd,uBAAuB,CAyenC;+BA7gBY,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW;mCAInC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,IAAI,IAAI,CAAC;IAAC,UAAU,IAAI,IAAI,CAAC;IAAC,YAAY,IAAI,IAAI,CAAC;IAAC,OAAO,IAAI,IAAI,CAAC;IAAC,aAAa,IAAI,IAAI,CAAC;IAAC,iBAAiB,IAAI,IAAI,CAAC;IAAC,oBAAoB,IAAI,IAAI,CAAC;IAAC,eAAe,IAAI,IAAI,CAAC;IAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;CAAE;8BAIhW;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;sCAInC,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW,GAAG,oBAAoB,CAAC,GAAG;IACvE,MAAM,IAAI,IAAI,CAAC;IACnB,QAAY,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;CAC9D;qBA5CmE,mBAAmB;2BAAnB,mBAAmB;wBAAnB,mBAAmB"}
package/dist/elena.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @elenajs/core v1.0.0-rc.10
2
+ * @elenajs/core v1.0.0-rc.12
3
3
  * (c) 2025-present Ariel Salminen and Elena contributors
4
4
  * @license MIT
5
5
  */
6
- import{getProps as t,setProps as e,getPropValue as s,syncAttribute as i}from"./props.js";import{warn as n,defineElement as r,prefix as o}from"./utils.js";export{html,nothing,unsafeHTML}from"./utils.js";import{renderTemplate as h}from"./render.js";const a=new WeakSet,d=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);function l(l){return class extends l{element=null;attributeChangedCallback(e,s,i){super.attributeChangedCallback?.(e,s,i),"text"!==e?(this._syncing=!0,t(this,e,s,i),this._syncing=!1,this._hydrated&&s!==i&&!this._isRendering&&this._safeRender()):this.text=i??""}static get observedAttributes(){if(this._observedAttrs)return this._observedAttrs;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this._observedAttrs=[...t,"text"],this._observedAttrs}connectedCallback(){super.connectedCallback?.(),this._setupStaticProps(),this._captureClassFieldDefaults(),this._hydrated||void 0!==this._text||(this.text=this.textContent.trim()),this._attachShadow(),this.willUpdate(),this._applyRender(),this._syncProps(),this._delegateEvents(),this._hydrated||(this._hydrated=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}_setupStaticProps(){const t=this.constructor;if(a.has(t))return;const s=new Set,i=[];if(t.props){for(const e of t.props)"string"==typeof e?i.push(e):(i.push(e.name),!1===e.reflect&&s.add(e.name));i.includes("text")&&n('"text" is reserved.'),e(t.prototype,i,s)}if(t._propNames=i,t._noReflect=s,t._elenaEvents=t.events||null,t._elenaEvents)for(const e of t._elenaEvents)d(t.prototype,e)||(t.prototype[e]=function(...t){return this.element[e](...t)});var r;t._resolver=(r=t.element)?t=>t.querySelector(r):t=>t.firstElementChild,a.add(t)}_captureClassFieldDefaults(){this._syncing=!0;for(const t of this.constructor._propNames)if(d(this,t)){const e=this[t];delete this[t],this[t]=e}this._syncing=!1}get _renderRoot(){return this._shadow??this.shadowRoot??this}_attachShadow(){const t=this.constructor;if(!t.shadow)return;this._shadow||this.shadowRoot||(this._shadow=this.attachShadow({mode:t.shadow}));const e=this._shadow??this.shadowRoot;if(t.styles){if(!t._adoptedSheets){const e=[t.styles].flat();t._adoptedSheets=e.map(t=>{if("string"==typeof t){const e=new CSSStyleSheet;return e.replaceSync(t),e}return t})}e.adoptedStyleSheets=t._adoptedSheets}}_applyRender(){const t=this.constructor,e=this._renderRoot,s=this.render();if(s&&s.strings&&h(e,s.strings,s.values)){const s=this.element;if(this.element=t._resolver(e),this._events&&s&&this.element!==s){const e=t._elenaEvents;for(const t of e)s.removeEventListener(t,this),this.element.addEventListener(t,this)}}this.element||(this.element=t._resolver(e),this.element||(t.element&&n("Element not found."),this.element=e.firstElementChild))}_syncProps(){if(this._props){const t=this.constructor._noReflect;for(const[e,n]of this._props){if(t.has(e))continue;const r=s(typeof n,n,"toAttribute");(null!==r||this.hasAttribute(e))&&i(this,e,r)}}}_delegateEvents(){const t=this.constructor._elenaEvents;if(!this._events&&t?.length)if(this.element){this._events=!0;for(const e of t)this.element.addEventListener(e,this)}else n("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this._events){this._events=!1;for(const t of this.constructor._elenaEvents)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor._elenaEvents?.includes(t.type)&&(t.bubbles&&(t.composed||this._renderRoot===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this._text??""}set text(t){const e=this._text;this._text=t,this._hydrated&&e!==t&&!this._isRendering&&this._safeRender()}static define(){const t=this.tagName;t?r(t,this):n("define() without a tagName.")}_safeRender(){this._isRendering||this._renderPending||(this._renderPending=!0,this._updateComplete=new Promise(t=>{this._resolveUpdate=t}),queueMicrotask(()=>{try{this._performUpdate()}catch(t){console.error(o,t)}}))}_performUpdate(){this._renderPending=!1;const t=this._resolveUpdate;this._resolveUpdate=null;try{try{this.willUpdate(),this._isRendering=!0,this._applyRender()}finally{this._isRendering=!1}this.updated()}finally{this._updateComplete=null,t()}}get updateComplete(){return this._updateComplete||Promise.resolve()}requestUpdate(){this._hydrated&&!this._isRendering&&this._safeRender()}}}export{l as Elena};
6
+ import{getPropValue as t,getProps as e,setProps as s,syncAttribute as i}from"./props.js";import{prefix as n,warn as r,defineElement as o}from"./utils.js";export{html,nothing,unsafeHTML}from"./utils.js";import{renderTemplate as h}from"./render.js";const a=new WeakSet,d=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);function l(l){return class extends l{element=null;attributeChangedCallback(s,i,n){if(super.attributeChangedCallback?.(s,i,n),"text"!==s){if(i!==n)if(this._hydrated&&!this._isRendering){const e=this._props.get(s),i=typeof e,r="string"===i?n??"":t(i,n,"toProp");r!==e&&this._props.set(s,r),this._safeRender()}else this._syncing=!0,e(this,s,i,n),this._syncing=!1}else this.text=n??""}static get observedAttributes(){if(this._observedAttrs)return this._observedAttrs;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this._observedAttrs=[...t,"text"],this._observedAttrs}connectedCallback(){super.connectedCallback?.(),this._setupStaticProps(),this._captureClassFieldDefaults(),this._hydrated||void 0!==this._text||(this.text=this.textContent.trim()),this._attachShadow(),this._root=this._shadow??this.shadowRoot??this,this._runUpdate??=()=>{try{this._performUpdate()}catch(t){console.error(n,t)}},this.willUpdate(),this._applyRender(),this._syncProps(),this._delegateEvents(),this._hydrated||(this._hydrated=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}_setupStaticProps(){const t=this.constructor;if(a.has(t))return;const e=new Set,i=[];if(t.props){for(const s of t.props)"string"==typeof s?i.push(s):(i.push(s.name),!1===s.reflect&&e.add(s.name));i.includes("text")&&r('"text" is reserved.'),s(t.prototype,i,e)}if(t._propNames=i,t._noReflect=e,t._elenaEvents=t.events||null,t._elenaEvents)for(const e of t._elenaEvents)d(t.prototype,e)||(t.prototype[e]=function(...t){return this.element[e](...t)});var n;t._resolver=(n=t.element)?t=>t.querySelector(n):t=>t.firstElementChild,a.add(t)}_captureClassFieldDefaults(){this._syncing=!0;for(const t of this.constructor._propNames)if(d(this,t)){const e=this[t];delete this[t],this[t]=e}this._syncing=!1}_attachShadow(){const t=this.constructor;if(!t.shadow)return;this._shadow||this.shadowRoot||(this._shadow=this.attachShadow({mode:t.shadow}));const e=this._shadow??this.shadowRoot;if(t.styles){if(!t._adoptedSheets){const e=[t.styles].flat();t._adoptedSheets=e.map(t=>{if("string"==typeof t){const e=new CSSStyleSheet;return e.replaceSync(t),e}return t})}e.adoptedStyleSheets=t._adoptedSheets}}_applyRender(){const t=this.constructor,e=this._root,s=this.render();if(s&&s.strings&&h(e,s.strings,s.values)){const s=this.element;if(this.element=t._resolver(e),this._events&&s&&this.element!==s){const e=t._elenaEvents;for(const t of e)s.removeEventListener(t,this),this.element.addEventListener(t,this)}}this.element||(this.element=t._resolver(e),this.element||(t.element&&r("Element not found."),this.element=e.firstElementChild))}_syncProps(){if(this._props){const e=this.constructor._noReflect;for(const[s,n]of this._props){if(e.has(s))continue;const r=t(typeof n,n,"toAttribute");(null!==r||this.hasAttribute(s))&&i(this,s,r)}}}_delegateEvents(){const t=this.constructor._elenaEvents;if(!this._events&&t?.length)if(this.element){this._events=!0;for(const e of t)this.element.addEventListener(e,this)}else r("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this._events){this._events=!1;for(const t of this.constructor._elenaEvents)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor._elenaEvents?.includes(t.type)&&(t.bubbles&&(t.composed||this._root===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this._text??""}set text(t){const e=this._text;this._text=t,this._hydrated&&e!==t&&!this._isRendering&&this._safeRender()}static define(){const t=this.tagName;t?o(t,this):r("define() without a tagName.")}_safeRender(){this._isRendering||this._renderPending||(this._renderPending=!0,queueMicrotask(this._runUpdate))}_performUpdate(){this._renderPending=!1;const t=this._resolveUpdate;this._resolveUpdate=null;try{try{this.willUpdate(),this._isRendering=!0,this._applyRender()}finally{this._isRendering=!1}this.updated()}finally{this._updateComplete=null,t?.()}}get updateComplete(){return this._renderPending?(this._updateComplete||(this._updateComplete=new Promise(t=>{this._resolveUpdate=t})),this._updateComplete):Promise.resolve()}requestUpdate(){this._hydrated&&!this._isRendering&&this._safeRender()}}}export{l as Elena};
package/dist/render.js CHANGED
@@ -1 +1 @@
1
- import{isArray as t,toPlainText as e,isRaw as n,collapseWhitespace as r,resolveValue as o}from"./utils.js";const a=new WeakMap,l="e"+(1e5*Math.random()|0),c=()=>document.createElement("template"),s=t=>document.createTreeWalker(t,128);function i(i,p,d){return!function(r,o,a){if(r._templateStrings!==o||!r._templateParts)return!1;for(let o=0;o<a.length;o++){const l=a[o],c=t(l)?e(l):l;if(c!==r._templateValues[o]){if(n(l)||!r._templateParts[o])return!1;r._templateValues[o]=c,r._templateParts[o].textContent=e(l)}}return!0}(i,p,d)&&(function(i,p,d){let f=a.get(p);if(!f){const t=p.map(r);f={_strings:t,_template:d.length>0?u(t,d.length):null},a.set(p,f)}if(f._template)i._templateParts=function(t,r,a){const i=r.content.cloneNode(!0),u=s(i),m=Array(a.length),p=[];let d;for(;d=u.nextNode();)d.data===l&&p.push(d);for(let t=0;t<p.length;t++){const r=a[t];if(n(r)){const e=c();e.innerHTML=o(r),p[t].parentNode.replaceChild(e.content,p[t])}else{const n=document.createTextNode(e(r));p[t].parentNode.replaceChild(n,p[t]),m[t]=n}}return t.replaceChildren(i),m}(i,f._template,d);else{const t=d.map(o),e=f._strings.reduce((e,n,r)=>e+n+(t[r]??""),"").replace(/>\s+</g,"><").trim(),n=c();n.innerHTML=e,m(i,n.content.childNodes),i._templateParts=null}i._templateStrings=p,i._templateValues=d.map(n=>t(n)?e(n):n)}(i,p,d),!0)}function u(t,e){const n=`\x3c!--${l}--\x3e`,r=t.reduce((t,r,o)=>t+r+(o<e?n:""),"").trim(),o=c();o.innerHTML=r;const a=s(o.content);let i=0;for(;a.nextNode();)a.currentNode.data===l&&i++;return i===e?o:null}function m(t,e){const n=Array.from(t.childNodes),r=Array.from(e),o=Math.max(n.length,r.length);for(let e=0;e<o;e++){const o=n[e],a=r[e];o?a?o.nodeType!==a.nodeType||1===o.nodeType&&o.tagName!==a.tagName?t.replaceChild(a,o):3===o.nodeType?o.textContent!==a.textContent&&(o.textContent=a.textContent):1===o.nodeType&&(p(o,a),m(o,a.childNodes)):t.removeChild(o):t.appendChild(a)}}function p(t,e){for(let n=t.attributes.length-1;n>=0;n--){const{name:r}=t.attributes[n];e.hasAttribute(r)||t.removeAttribute(r)}for(let n=0;n<e.attributes.length;n++){const{name:r,value:o}=e.attributes[n];t.getAttribute(r)!==o&&t.setAttribute(r,o)}}export{i as renderTemplate};
1
+ import{isArray as t,toPlainText as e,isRaw as n,nothing as r,collapseWhitespace as o,resolveValue as l}from"./utils.js";const s=new WeakMap,a="e"+Math.random().toString(36).slice(2),c=()=>document.createElement("template"),i=t=>document.createTreeWalker(t,128);function u(u,m,f){return!function(o,l,s){if(o._templateStrings!==l||!o._templateParts)return!1;const a=o._templateParts,c=o._templateValues;for(let o=0;o<s.length;o++){const l=s[o],i=t(l)?e(l):l;if(i===c[o])continue;if(n(l)&&l!==r)return!1;const u=a[o];if(!u)return!1;c[o]=i;const p=String(i??"");u.nodeType?u.textContent=p:u[0].setAttribute(u[1],p)}return!0}(u,m,f)&&(function(u,m,f){let h=s.get(m);if(!h){const t=m.map(o);h={_strings:t,_template:f.length>0?p(t,f.length):null},s.set(m,h)}if(h._template)u._templateParts=function(o,s,u){const{_tpl:p,_attrs:m}=s,f=p.content.cloneNode(!0),h=i(f),g=Array(u.length),_=[];let N;for(;N=h.nextNode();)N.data===a&&_.push(N);let x=0;for(let o=0;o<u.length;o++){const s=m[o];if(s){const n=f.querySelector(`[${s}="${a+"_"+o}"]`);if(n){const r=u[o],l=String((t(r)?e(r):r)??"");n.setAttribute(s,l),g[o]=[n,s]}}else{const t=_[x++],s=u[o];if(n(s)&&s!==r){const e=c();e.innerHTML=l(s),t.parentNode.replaceChild(e.content,t)}else{const n=document.createTextNode(e(s));t.parentNode.replaceChild(n,t),g[o]=n}}}return o._templateStrings?(d(o,f.childNodes),null):(o.replaceChildren(f),g)}(u,h._template,f);else{const t=f.map(l),e=h._strings.reduce((e,n,r)=>e+n+(t[r]??""),"").replace(/>\s+</g,"><").trim(),n=c();n.innerHTML=e,d(u,n.content.childNodes),u._templateParts=null}u._templateStrings=m,u._templateValues=f.map(n=>t(n)?e(n):n)}(u,m,f),!0)}function p(t,e){const n=`\x3c!--${a}--\x3e`,r=[];let o="";for(let l=0;l<t.length;l++)if(o+=t[l],l<e){const e=t[l].match(/([^\s"'>/=]+)\s*=\s*["']$/);e?(r.push(e[1]),o+=a+"_"+l):(r.push(null),o+=n)}const l=c();l.innerHTML=o.trim();const s=i(l.content);let u=0;for(;s.nextNode();)s.currentNode.data===a&&u++;return u!==r.filter(t=>null===t).length?null:{_tpl:l,_attrs:r}}function d(t,e){const n=Array.from(t.childNodes),r=Array.from(e),o=Math.max(n.length,r.length);for(let e=0;e<o;e++){const o=n[e],l=r[e];o?l?o.nodeType!==l.nodeType||1===o.nodeType&&o.tagName!==l.tagName?t.replaceChild(l,o):3===o.nodeType?o.textContent!==l.textContent&&(o.textContent=l.textContent):1===o.nodeType&&(m(o,l),d(o,l.childNodes)):t.removeChild(o):t.appendChild(l)}}function m(t,e){for(let n=t.attributes.length-1;n>=0;n--){const{name:r}=t.attributes[n];e.hasAttribute(r)||t.removeAttribute(r)}for(let n=0;n<e.attributes.length;n++){const{name:r,value:o}=e.attributes[n];t.getAttribute(r)!==o&&t.setAttribute(r,o)}}export{u as renderTemplate};
package/dist/utils.js CHANGED
@@ -1 +1 @@
1
- const n="░█ [ELENA]: ",r=Array.isArray,t=r=>console.warn(n+r);function e(n,r){const t=globalThis.customElements;t?.get(n)||t?.define(n,r)}const o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function i(n){return String(n).replace(/[&<>"']/g,n=>o[n])}function c(n){return r(n)?n.map(u).join(""):u(n)}function u(n){return n?.__raw?String(n):i(n??"")}function a(n,...r){let t;return{__raw:!0,strings:n,values:r,toString:()=>(null==t&&(t=n.reduce((n,t,e)=>n+t+c(r[e]),"")),t)}}function s(n){return{__raw:!0,toString:()=>n??""}}const g={__raw:!0,toString:()=>""},l=n=>r(n)?n.some(n=>n?.__raw):n?.__raw,_=n=>r(n)?n.join(""):String(n??"");function f(n){return n.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}export{f as collapseWhitespace,e as defineElement,i as escapeHtml,a as html,r as isArray,l as isRaw,g as nothing,n as prefix,c as resolveValue,_ as toPlainText,s as unsafeHTML,t as warn};
1
+ const t="░█ [ELENA]: ",n=Array.isArray,r=Symbol("elena.raw"),s=n=>console.warn(t+n);function e(t,n){const r=globalThis.customElements;r?.get(t)||r?.define(t,n)}const o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function i(t){return String(t).replace(/[&<>"']/g,t=>o[t])}function c(t){return n(t)?t.map(u).join(""):u(t)}function u(t){return t?.[r]?String(t):i(t??"")}class l{constructor(t,n){this.strings=t,this.values=n}toString(){return null==this._str&&(this._str=this.strings.reduce((t,n,r)=>t+n+c(this.values[r]),"")),this._str}}function a(t,...n){return new l(t,n)}function g(t){return{[r]:!0,toString:()=>t??""}}l.prototype[r]=!0;const p={[r]:!0,toString:()=>""},f=t=>n(t)?t.some(t=>t?.[r]):!!t?.[r],h=t=>n(t)?t.join(""):String(t??"");function S(t){return t.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}export{S as collapseWhitespace,e as defineElement,i as escapeHtml,a as html,n as isArray,f as isRaw,p as nothing,t as prefix,c as resolveValue,h as toPlainText,g as unsafeHTML,s as warn};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elenajs/core",
3
- "version": "1.0.0-rc.10",
3
+ "version": "1.0.0-rc.12",
4
4
  "description": "Elena is a simple, tiny library for building Progressive Web Components.",
5
5
  "author": "Elena <hi@elenajs.com>",
6
6
  "homepage": "https://elenajs.com/",
@@ -56,5 +56,5 @@
56
56
  "typescript": "6.0.2",
57
57
  "vitest": "4.1.1"
58
58
  },
59
- "gitHead": "89275a58542d7a86e64b6052416a935c89d90d52"
59
+ "gitHead": "6e8de235d2bb9de8028c920b4213dc40505b4ef4"
60
60
  }
@@ -68,7 +68,7 @@ export function syncAttribute(element, name, value) {
68
68
  * at class-creation time. Values are stored per-instance
69
69
  * via a `_props` Map that is lazily created.
70
70
  *
71
- * @param {Function} proto - The class prototype
71
+ * @param {Object} proto - The class prototype
72
72
  * @param {string[]} propNames - Prop names to define
73
73
  * @param {Set<string>} [noReflect] - Props that should not reflect to attributes
74
74
  */
@@ -1,7 +1,7 @@
1
- import { collapseWhitespace, isArray, isRaw, resolveValue, toPlainText } from "./utils.js";
1
+ import { collapseWhitespace, isArray, isRaw, nothing, resolveValue, toPlainText } from "./utils.js";
2
2
 
3
3
  const stringsCache = new WeakMap();
4
- const markerKey = "e" + ((Math.random() * 1e5) | 0);
4
+ const markerKey = "e" + Math.random().toString(36).slice(2);
5
5
  const SHOW_COMMENT = 128;
6
6
  const ELEMENT_NODE = 1;
7
7
  const TEXT_NODE = 3;
@@ -11,8 +11,8 @@ const treeWalker = node => document.createTreeWalker(node, SHOW_COMMENT);
11
11
 
12
12
  /**
13
13
  * Render a tagged template into an Elena Element with DOM diffing.
14
- * Returns true if the DOM was fully rebuilt, false if only text
15
- * nodes were patched in place.
14
+ * Returns true if the DOM was fully rebuilt, false if parts were
15
+ * patched in place.
16
16
  *
17
17
  * @param {HTMLElement} element
18
18
  * @param {TemplateStringsArray} strings - Static parts of the tagged template
@@ -20,54 +20,69 @@ const treeWalker = node => document.createTreeWalker(node, SHOW_COMMENT);
20
20
  * @returns {boolean}
21
21
  */
22
22
  export function renderTemplate(element, strings, values) {
23
- if (patchTextNodes(element, strings, values)) {
23
+ if (patch(element, strings, values)) {
24
24
  return false;
25
25
  }
26
- fullRender(element, strings, values);
26
+ morph(element, strings, values);
27
27
  return true;
28
28
  }
29
29
 
30
30
  /**
31
- * Fast path: patch only the text nodes whose values changed.
31
+ * Patch only changed text nodes and attribute values.
32
32
  *
33
33
  * @param {HTMLElement} element - The host element with cached template state
34
34
  * @param {TemplateStringsArray} strings - Static parts of the tagged template
35
35
  * @param {Array} values - Dynamic interpolated values
36
- * @returns {boolean} Whether patching was sufficient (false = full render)
36
+ * @returns {boolean} Whether patching was sufficient (false = do morph instead)
37
37
  */
38
- function patchTextNodes(element, strings, values) {
38
+ function patch(element, strings, values) {
39
39
  // Only works when re-rendering the same template shape
40
40
  if (element._templateStrings !== strings || !element._templateParts) {
41
41
  return false;
42
42
  }
43
43
 
44
+ const parts = element._templateParts;
45
+ const cached = element._templateValues;
46
+
44
47
  for (let i = 0; i < values.length; i++) {
45
48
  const v = values[i];
46
49
  const comparable = isArray(v) ? toPlainText(v) : v;
47
50
 
48
- if (comparable === element._templateValues[i]) {
51
+ if (comparable === cached[i]) {
49
52
  continue;
50
53
  }
51
54
 
52
- if (isRaw(v) || !element._templateParts[i]) {
55
+ if (isRaw(v) && v !== nothing) {
56
+ return false;
57
+ }
58
+
59
+ const part = parts[i];
60
+
61
+ if (!part) {
53
62
  return false;
54
63
  }
55
64
 
56
- element._templateValues[i] = comparable;
57
- element._templateParts[i].textContent = toPlainText(v);
65
+ cached[i] = comparable;
66
+ const str = String(comparable ?? "");
67
+
68
+ if (part.nodeType) {
69
+ part.textContent = str;
70
+ } else {
71
+ part[0].setAttribute(part[1], str);
72
+ }
58
73
  }
59
74
 
60
75
  return true;
61
76
  }
62
77
 
63
78
  /**
64
- * Cold path: clone a cached <template> and patch in values.
79
+ * Clone a cached <template> and morph in new structure.
65
80
  *
66
81
  * @param {HTMLElement} element - The host element to render into
67
82
  * @param {TemplateStringsArray} strings - Static parts of the tagged template
68
83
  * @param {Array} values - Dynamic interpolated values
69
84
  */
70
- function fullRender(element, strings, values) {
85
+ function morph(element, strings, values) {
71
86
  let entry = stringsCache.get(strings);
72
87
 
73
88
  if (!entry) {
@@ -82,7 +97,7 @@ function fullRender(element, strings, values) {
82
97
  if (entry._template) {
83
98
  element._templateParts = cloneAndPatch(element, entry._template, values);
84
99
  } else {
85
- // Fallback for attribute-position values or static templates.
100
+ // Fallback for static templates or templates where marker detection failed.
86
101
  // White space collapsing here protects against Vue SSR mismatches.
87
102
  const renderedValues = values.map(resolveValue);
88
103
  const markup = entry._strings
@@ -102,45 +117,66 @@ function fullRender(element, strings, values) {
102
117
  }
103
118
 
104
119
  /**
105
- * Build a <template> element with comment markers.
120
+ * Create a <template> element with comment markers and string placeholders.
106
121
  *
107
122
  * @param {string[]} _strings - Whitespace-collapsed static parts
108
123
  * @param {number} valueCount - Number of dynamic values
109
- * @returns {HTMLTemplateElement | null}
124
+ * @returns {{ _tpl: HTMLTemplateElement, _attrs: (string|null)[] } | null}
110
125
  */
111
126
  function createTemplate(_strings, valueCount) {
112
127
  const marker = `<!--${markerKey}-->`;
113
- const markup = _strings
114
- .reduce((out, str, i) => out + str + (i < valueCount ? marker : ""), "")
115
- .trim();
128
+ const attrs = [];
129
+ let markup = "";
130
+
131
+ for (let i = 0; i < _strings.length; i++) {
132
+ markup += _strings[i];
133
+
134
+ if (i < valueCount) {
135
+ const match = _strings[i].match(/([^\s"'>/=]+)\s*=\s*["']$/);
136
+
137
+ if (match) {
138
+ attrs.push(match[1]);
139
+ markup += markerKey + "_" + i;
140
+ } else {
141
+ attrs.push(null);
142
+ markup += marker;
143
+ }
144
+ }
145
+ }
116
146
 
117
147
  const template = newTemplate();
118
- template.innerHTML = markup;
148
+ template.innerHTML = markup.trim();
119
149
 
120
150
  // Mismatch means this template shape cannot use the clone path.
121
151
  const walker = treeWalker(template.content);
122
- let count = 0;
152
+ let commentCount = 0;
123
153
 
124
154
  while (walker.nextNode()) {
125
155
  if (walker.currentNode.data === markerKey) {
126
- count++;
156
+ commentCount++;
127
157
  }
128
158
  }
129
159
 
130
- return count === valueCount ? template : null;
160
+ const expectedComments = attrs.filter(n => n === null).length;
161
+
162
+ if (commentCount !== expectedComments) {
163
+ return null;
164
+ }
165
+
166
+ return { _tpl: template, _attrs: attrs };
131
167
  }
132
168
 
133
169
  /**
134
- * Clone a cached template and replace comment markers
135
- * with actual content.
170
+ * Clone a cached template and replace markers with actual content.
136
171
  *
137
172
  * @param {HTMLElement} element - The host element to render into
138
- * @param {HTMLTemplateElement} template - Cached template with markers
173
+ * @param {{ _tpl: HTMLTemplateElement, _attrs: (string|null)[] }} templateInfo
139
174
  * @param {Array} values - Raw interpolated values
140
- * @returns {Array<Text | undefined>} Text node map for fast-path patching
175
+ * @returns {Array<Text | [Element, string] | undefined> | null}
141
176
  */
142
- function cloneAndPatch(element, template, values) {
143
- const clone = template.content.cloneNode(true);
177
+ function cloneAndPatch(element, templateInfo, values) {
178
+ const { _tpl, _attrs } = templateInfo;
179
+ const clone = _tpl.content.cloneNode(true);
144
180
  const walker = treeWalker(clone);
145
181
  const parts = Array(values.length);
146
182
  const markers = [];
@@ -153,24 +189,46 @@ function cloneAndPatch(element, template, values) {
153
189
  }
154
190
  }
155
191
 
156
- for (let i = 0; i < markers.length; i++) {
157
- const value = values[i];
192
+ let contentIdx = 0;
158
193
 
159
- if (isRaw(value)) {
160
- // Raw HTML: parse and insert as fragment
161
- const tmp = newTemplate();
162
- tmp.innerHTML = resolveValue(value);
163
- markers[i].parentNode.replaceChild(tmp.content, markers[i]);
164
-
165
- // Raw values can't be fast-patched; leave parts undefined
194
+ for (let i = 0; i < values.length; i++) {
195
+ const attr = _attrs[i];
196
+
197
+ if (attr) {
198
+ // Find the element with the placeholder value
199
+ const placeholder = markerKey + "_" + i;
200
+ const el = clone.querySelector(`[${attr}="${placeholder}"]`);
201
+
202
+ if (el) {
203
+ const value = values[i];
204
+ const str = String((isArray(value) ? toPlainText(value) : value) ?? "");
205
+ el.setAttribute(attr, str);
206
+ parts[i] = [el, attr];
207
+ }
166
208
  } else {
167
- // Create text node with unescaped content
168
- const textNode = document.createTextNode(toPlainText(value));
169
- markers[i].parentNode.replaceChild(textNode, markers[i]);
170
- parts[i] = textNode;
209
+ // Replace comment marker with value
210
+ const marker = markers[contentIdx++];
211
+ const value = values[i];
212
+
213
+ // Parse and insert raw HTML as a fragment
214
+ if (isRaw(value) && value !== nothing) {
215
+ const tmp = newTemplate();
216
+ tmp.innerHTML = resolveValue(value);
217
+ marker.parentNode.replaceChild(tmp.content, marker);
218
+
219
+ // Create text node with unescaped content
220
+ } else {
221
+ const textNode = document.createTextNode(toPlainText(value));
222
+ marker.parentNode.replaceChild(textNode, marker);
223
+ parts[i] = textNode;
224
+ }
171
225
  }
172
226
  }
173
227
 
228
+ if (element._templateStrings) {
229
+ morphContent(element, clone.childNodes);
230
+ return null;
231
+ }
174
232
  element.replaceChildren(clone);
175
233
  return parts;
176
234
  }
@@ -212,7 +270,7 @@ function morphContent(parent, nextNodes) {
212
270
  }
213
271
 
214
272
  /**
215
- * Morhp element’s attributes without rebuilding the DOM.
273
+ * Morph element’s attributes without rebuilding the DOM.
216
274
  *
217
275
  * @param {Element} current - The current existing DOM element
218
276
  * @param {Element} next - The desired element from the new render
@@ -1,5 +1,6 @@
1
1
  const prefix = "░█ [ELENA]: ";
2
2
  const isArray = Array.isArray;
3
+ const RAW = Symbol("elena.raw");
3
4
 
4
5
  /**
5
6
  * @param {string} msg
@@ -52,51 +53,59 @@ export function resolveValue(value) {
52
53
  * @returns {string}
53
54
  */
54
55
  function resolveItem(value) {
55
- return value?.__raw ? String(value) : escapeHtml(value ?? "");
56
+ return value?.[RAW] ? String(value) : escapeHtml(value ?? "");
56
57
  }
57
58
 
59
+ /**
60
+ * Lightweight template result.
61
+ *
62
+ * @internal
63
+ */
64
+ class HtmlResult {
65
+ constructor(strings, values) {
66
+ this.strings = strings;
67
+ this.values = values;
68
+ }
69
+ toString() {
70
+ if (this._str == null) {
71
+ this._str = this.strings.reduce((acc, s, i) => {
72
+ return acc + s + resolveValue(this.values[i]);
73
+ }, "");
74
+ }
75
+ return this._str;
76
+ }
77
+ }
78
+ HtmlResult.prototype[RAW] = true;
79
+
58
80
  /**
59
81
  * Tagged template for trusted HTML. Use as the return value
60
82
  * of render(), or for sub-fragments inside render methods.
61
83
  *
62
84
  * @param {TemplateStringsArray} strings
63
85
  * @param {...*} values
64
- * @returns {{ __raw: true, strings: TemplateStringsArray, values: Array, toString(): string }}
86
+ * @returns {{ strings: TemplateStringsArray, values: Array, toString(): string }}
65
87
  */
66
88
  export function html(strings, ...values) {
67
- let str;
68
- return {
69
- __raw: true,
70
- strings,
71
- values,
72
- toString: () => {
73
- if (str == null) {
74
- str = strings.reduce((acc, s, i) => {
75
- return acc + s + resolveValue(values[i]);
76
- }, "");
77
- }
78
- return str;
79
- },
80
- };
89
+ return new HtmlResult(strings, values);
81
90
  }
82
91
 
83
92
  /**
84
93
  * Renders a string as HTML rather than text.
85
94
  *
86
95
  * @param {string} str - The raw HTML string to trust.
87
- * @returns {{ __raw: true, toString(): string }}
96
+ * @returns {{ toString(): string }}
88
97
  */
89
98
  export function unsafeHTML(str) {
90
- return { __raw: true, toString: () => str ?? "" };
99
+ return { [RAW]: true, toString: () => str ?? "" };
91
100
  }
92
101
 
93
102
  /**
94
103
  * A placeholder you can return from a conditional expression
95
104
  * inside a template to render nothing.
96
105
  *
97
- * @type {{ __raw: true, toString(): string }}
106
+ * @type {{ toString(): string }}
98
107
  */
99
- export const nothing = { __raw: true, toString: () => "" };
108
+ export const nothing = { [RAW]: true, toString: () => "" };
100
109
 
101
110
  /**
102
111
  * Check if a value contains trusted HTML fragments.
@@ -104,7 +113,7 @@ export const nothing = { __raw: true, toString: () => "" };
104
113
  * @param {*} value
105
114
  * @returns {boolean}
106
115
  */
107
- export const isRaw = value => (isArray(value) ? value.some(item => item?.__raw) : value?.__raw);
116
+ export const isRaw = value => (isArray(value) ? value.some(item => item?.[RAW]) : !!value?.[RAW]);
108
117
 
109
118
  /**
110
119
  * Convert a value to its plain text string.
package/src/elena.js CHANGED
@@ -40,7 +40,7 @@ function elementResolver(selector) {
40
40
  */
41
41
 
42
42
  /**
43
- * @typedef {{ text: string, element: HTMLElement | null, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, connectedCallback(): void, disconnectedCallback(): void }} ElenaInstanceMembers
43
+ * @typedef {{ text: string, element: HTMLElement | null, updateComplete: Promise<void>, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, requestUpdate(): void, connectedCallback(): void, disconnectedCallback(): void, adoptedCallback(): void, attributeChangedCallback(prop: string, oldValue: string | null, newValue: string | null): void }} ElenaInstanceMembers
44
44
  */
45
45
 
46
46
  /**
@@ -91,8 +91,8 @@ export function Elena(superClass) {
91
91
  * Updates the matching prop and re-renders if needed.
92
92
  *
93
93
  * @param {string} prop
94
- * @param {string} oldValue
95
- * @param {string} newValue
94
+ * @param {string | null} oldValue
95
+ * @param {string | null} newValue
96
96
  */
97
97
  attributeChangedCallback(prop, oldValue, newValue) {
98
98
  super.attributeChangedCallback?.(prop, oldValue, newValue);
@@ -102,17 +102,29 @@ export function Elena(superClass) {
102
102
  return;
103
103
  }
104
104
 
105
- // Set flag so the property setter skips redundant attribute reflection:
106
- // the attribute is already at the new value, no need to set it again.
107
- this._syncing = true;
108
- getProps(this, prop, oldValue, newValue);
109
- this._syncing = false;
105
+ if (oldValue === newValue) {
106
+ return;
107
+ }
110
108
 
111
- // Re-render when attributes change (after initial render).
112
- // Guard against re-entrant renders: if render() itself mutates an observed
113
- // attribute, skip the recursive call to prevent an infinite loop.
114
- if (this._hydrated && oldValue !== newValue && !this._isRendering) {
109
+ if (this._hydrated && !this._isRendering) {
110
+ // The attribute is already set and we just need the coerced
111
+ // prop value stored for the next render.
112
+ const current = this._props.get(prop);
113
+ const type = typeof current;
114
+ const coerced =
115
+ type === "string" ? (newValue ?? "") : getPropValue(type, newValue, "toProp");
116
+
117
+ if (coerced !== current) {
118
+ this._props.set(prop, coerced);
119
+ }
115
120
  this._safeRender();
121
+
122
+ // Runs pre-hydration or during render.
123
+ // Goes through the setter so _props is initialized correctly.
124
+ } else {
125
+ this._syncing = true;
126
+ getProps(this, prop, oldValue, newValue);
127
+ this._syncing = false;
116
128
  }
117
129
  }
118
130
 
@@ -141,6 +153,16 @@ export function Elena(superClass) {
141
153
  this.text = this.textContent.trim();
142
154
  }
143
155
  this._attachShadow();
156
+ this._root = this._shadow ?? this.shadowRoot ?? this;
157
+
158
+ this._runUpdate ??= () => {
159
+ try {
160
+ this._performUpdate();
161
+ } catch (e) {
162
+ console.error(prefix, e);
163
+ }
164
+ };
165
+
144
166
  this.willUpdate();
145
167
  this._applyRender();
146
168
  this._syncProps();
@@ -228,16 +250,6 @@ export function Elena(superClass) {
228
250
  this._syncing = false;
229
251
  }
230
252
 
231
- /**
232
- * The root node to render into. Returns the shadow root when shadow mode
233
- * is enabled, otherwise the host element itself.
234
- *
235
- * @type {ShadowRoot | HTMLElement}
236
- */
237
- get _renderRoot() {
238
- return this._shadow ?? this.shadowRoot ?? this;
239
- }
240
-
241
253
  /**
242
254
  * Attaches a shadow root and adopts styles on first connect.
243
255
  * Only runs when `static shadow` is set on the component class.
@@ -290,14 +302,14 @@ export function Elena(superClass) {
290
302
  */
291
303
  _applyRender() {
292
304
  const constructor = this.constructor;
293
- const root = this._renderRoot;
305
+ const root = this._root;
294
306
  const result = this.render();
295
307
 
296
308
  if (result && result.strings) {
297
309
  const rebuilt = renderTemplate(root, result.strings, result.values);
298
310
 
299
311
  // Re-resolve element ref when the DOM was fully rebuilt.
300
- // Fast-path text node patching leaves the DOM structure intact,
312
+ // patch() and morph() leave the DOM structure intact,
301
313
  // so the existing ref is still valid.
302
314
  if (rebuilt) {
303
315
  const oldElement = this.element;
@@ -430,6 +442,7 @@ export function Elena(superClass) {
430
442
  * events in Shadow DOM (change, submit, reset).
431
443
  * Composed bubbling events (click, input) pass through on their own.
432
444
  *
445
+ * @param {Event} event
433
446
  * @internal
434
447
  */
435
448
  handleEvent(event) {
@@ -437,7 +450,7 @@ export function Elena(superClass) {
437
450
  return;
438
451
  }
439
452
 
440
- if (!event.bubbles || (!event.composed && this._renderRoot !== this)) {
453
+ if (!event.bubbles || (!event.composed && this._root !== this)) {
441
454
  /** @internal */
442
455
  this.dispatchEvent(new Event(event.type, { bubbles: event.bubbles }));
443
456
  }
@@ -489,16 +502,7 @@ export function Elena(superClass) {
489
502
  }
490
503
  if (!this._renderPending) {
491
504
  this._renderPending = true;
492
- this._updateComplete = new Promise(resolve => {
493
- this._resolveUpdate = resolve;
494
- });
495
- queueMicrotask(() => {
496
- try {
497
- this._performUpdate();
498
- } catch (e) {
499
- console.error(prefix, e);
500
- }
501
- });
505
+ queueMicrotask(this._runUpdate);
502
506
  }
503
507
  }
504
508
 
@@ -523,7 +527,7 @@ export function Elena(superClass) {
523
527
  this.updated();
524
528
  } finally {
525
529
  this._updateComplete = null;
526
- resolve();
530
+ resolve?.();
527
531
  }
528
532
  }
529
533
 
@@ -534,7 +538,15 @@ export function Elena(superClass) {
534
538
  * @type {Promise<void>}
535
539
  */
536
540
  get updateComplete() {
537
- return this._updateComplete || Promise.resolve();
541
+ if (!this._renderPending) {
542
+ return Promise.resolve();
543
+ }
544
+ if (!this._updateComplete) {
545
+ this._updateComplete = new Promise(resolve => {
546
+ this._resolveUpdate = resolve;
547
+ });
548
+ }
549
+ return this._updateComplete;
538
550
  }
539
551
 
540
552
  /**