@elenajs/core 1.0.0-rc.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
+ <br/>
1
2
  <div align="center">
2
3
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/img/elena-dark.png" alt="Elena" width="558" height="220">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/elena-v2-dark.png" alt="Elena" width="127" height="156">
4
5
  </source>
5
- <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
6
+ <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/elena-v3.png" alt="Elena" width="127" height="156">
6
7
  </source>
7
- <img src="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
8
+ <img src="https://elenajs.com/elena-v2.png" alt="Elena" width="127" height="156">
8
9
  </picture>
9
10
 
10
- ### Simple, tiny library for building Progressive Web Components.
11
+ ### Simple, tiny library for building Progressive Web Components
11
12
 
12
13
  <br/>
13
14
 
@@ -22,44 +23,80 @@
22
23
 
23
24
  <br/>
24
25
 
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>
26
+ <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>
27
+
28
+ ## Features
29
+
30
+ - 🔋 **Extremely lightweight:** 2.9kB minified & compressed, simple and tiny by design.
31
+ - 📈 **Progressively enhanced:** Renders HTML & CSS first, then hydrates with JavaScript.
32
+ - 🫶 **Accessible by default:** Semantic HTML foundation with no Shadow DOM barriers.
33
+ - 🌍 **Standards based:** Built entirely on native custom elements & web standards.
34
+ - ⚡ **Reactive updates:** Prop and state changes trigger efficient, batched re-renders.
35
+ - 🎨 **Scoped styles:** Simple & clean CSS encapsulation without complex workarounds.
36
+ - 🖥️ **SSR friendly:** Works out of the box, with optional server-side utilities if needed.
37
+ - 🧩 **Zero dependencies:** No runtime dependencies, runs entirely on the web platform.
38
+ - 🔓 **Zero lock-in:** Works with every major framework, or no framework at all.
26
39
 
27
- ## Documentation
40
+ ## Usage
28
41
 
29
- See the full documentation for Elena at [elenajs.com](https://elenajs.com).
42
+ To install Elena as a dependency, run:
30
43
 
31
44
  ```sh
32
45
  npm install @elenajs/core
33
46
  ```
34
47
 
35
- ## Packages
48
+ Then import Elena in a web component:
36
49
 
37
- Elena is a monorepo containing several packages published to npm under the `@elenajs` scope. These are the main packages intended for development:
50
+ ```js
51
+ import { Elena } from "@elenajs/core";
38
52
 
39
- | Package | Description | Version | Stability |
40
- | --- | --- | --- | --- |
41
- | [`@elenajs/core`](https://github.com/getelena/elena/tree/main/packages/core) | Elena core runtime library. | [![npm](https://img.shields.io/npm/v/@elenajs/core.svg)](https://www.npmjs.com/package/@elenajs/core) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
42
- | [`@elenajs/components`](https://github.com/getelena/elena/tree/main/packages/components) | Elena demo web components. | [![npm](https://img.shields.io/npm/v/@elenajs/components.svg)](https://www.npmjs.com/package/@elenajs/components) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
43
- | [`@elenajs/bundler`](https://github.com/getelena/elena/tree/main/packages/bundler) | Elena bundler for component libraries. | [![npm](https://img.shields.io/npm/v/@elenajs/bundler.svg)](https://www.npmjs.com/package/@elenajs/bundler) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
44
- | [`@elenajs/cli`](https://github.com/getelena/elena/tree/main/packages/cli) | Elena CLI for scaffolding web components. | [![npm](https://img.shields.io/npm/v/@elenajs/cli.svg)](https://www.npmjs.com/package/@elenajs/cli) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
45
- | [`@elenajs/plugin-cem-define`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-define) | Elena CEM Define plugin. | [![npm](https://img.shields.io/npm/v/@elenajs/plugin-cem-define.svg)](https://www.npmjs.com/package/@elenajs/plugin-cem-define) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
46
- | [`@elenajs/plugin-cem-prop`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-prop) | Elena CEM Prop plugin. | [![npm](https://img.shields.io/npm/v/@elenajs/plugin-cem-prop.svg)](https://www.npmjs.com/package/@elenajs/plugin-cem-prop) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
47
- | [`@elenajs/plugin-cem-tag`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-tag) | Elena CEM Tag plugin. | [![npm](https://img.shields.io/npm/v/@elenajs/plugin-cem-tag.svg)](https://www.npmjs.com/package/@elenajs/plugin-cem-tag) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
48
- | [`@elenajs/plugin-cem-typescript`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-typescript) | Elena CEM TypeScript plugin. | [![npm](https://img.shields.io/npm/v/@elenajs/plugin-cem-typescript.svg)](https://www.npmjs.com/package/@elenajs/plugin-cem-typescript) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
49
- | [`@elenajs/plugin-rollup-css`](https://github.com/getelena/elena/tree/main/packages/plugin-rollup-css) | Elena Rollup CSS plugin. | [![npm](https://img.shields.io/npm/v/@elenajs/plugin-rollup-css.svg)](https://www.npmjs.com/package/@elenajs/plugin-rollup-css) | ![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg) |
50
- | [`@elenajs/ssr`](https://github.com/getelena/elena/tree/main/packages/ssr) | Elena server-side rendering tools. | [![npm](https://img.shields.io/npm/v/@elenajs/ssr.svg)](https://www.npmjs.com/package/@elenajs/ssr) | ![stability-experimental](https://img.shields.io/badge/stability-experimental-red.svg) |
51
- | [`@elenajs/mcp`](https://github.com/getelena/elena/tree/main/packages/ssr) | Elena MCP server. | [![npm](https://img.shields.io/npm/v/@elenajs/mcp.svg)](https://www.npmjs.com/package/@elenajs/mcp) | ![stability-experimental](https://img.shields.io/badge/stability-experimental-red.svg) |
53
+ class Stack extends Elena(HTMLElement) {
54
+ static tagName = "my-stack";
55
+ static props = ["direction"];
52
56
 
53
- <!-- https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md -->
57
+ direction = "column";
58
+ }
54
59
 
55
- ## Development
60
+ Stack.define();
61
+ ```
56
62
 
57
- For more details about pull requests, commit conventions and code style, please see [CONTRIBUTING.md](CONTRIBUTING.md).
63
+ **See the full documentation at [elenajs.com](https://elenajs.com).**
58
64
 
59
- ## License
65
+ ## Why was Elena created
66
+
67
+ Elena was created by [@arielle](https://arielsalminen.com/) after nearly a decade of building enterprise-scale design systems with web components. The recurring pain points were often similar: accessibility issues, server-side rendering, layout shifts, flash of invisible content, React Server Components, too much reliance on client side JavaScript, and compatibility with e.g. third party analytics tools.
68
+
69
+ Elena was built to solve these problems while staying grounded in web standards and what the platform natively provides. This is how [Progressive Web Components](https://arielsalminen.com/2026/progressive-web-components/) were born.
70
+
71
+ ## Why should I use Elena
60
72
 
61
- MIT
73
+ **Elena is built for teams creating component libraries and design systems.** If you need web components that work across multiple frameworks (such as [React](https://react.dev), [Next.js](https://nextjs.org), [Vue](https://vuejs.org), [Angular](https://angular.dev)), render HTML and CSS before JavaScript loads, and sidestep common issues like accessibility problems, SSR limitations, and layout shifts, Elena is built for exactly that.
62
74
 
63
- ## Copyright
75
+ It handles the cross-framework complexity (prop/attribute syncing, event delegation, framework compatibility) so you can focus on building components rather than plumbing.
76
+
77
+ ## Next steps
78
+
79
+ - Start with the [Quick Start](https://elenajs.com/start/) guide.
80
+ - View the [Live examples](https://elenajs.com/examples/) for demos.
81
+ - Try Elena in the [Playground](https://elenajs.com/playground/).
82
+ - Read how [Elena compares](https://elenajs.com/advanced/faq#how-does-elena-compare-against-other-tools) against other web component libraries.
83
+ - Browse our [FAQ](https://elenajs.com/advanced/faq) for frequently asked questions.
84
+
85
+ ## Provided tools
86
+
87
+ Elena is a monorepo containing several tools (13 in total!) published to npm under the `@elenajs` scope. These are the main tools intended for development:
88
+
89
+ | Package | Description | Version | Stability |
90
+ | --- | --- | --- | --- |
91
+ | [`@elenajs/core`](https://github.com/getelena/elena/tree/main/packages/core) | Elena core runtime library. | [![npm](https://img.shields.io/npm/v/@elenajs/core.svg)](https://www.npmjs.com/package/@elenajs/core) | ![stability-stable](https://img.shields.io/badge/stability-stable-green.svg) |
92
+ | [`@elenajs/components`](https://github.com/getelena/elena/tree/main/packages/components) | Elena demo web components. | [![npm](https://img.shields.io/npm/v/@elenajs/components.svg)](https://www.npmjs.com/package/@elenajs/components) | ![stability-stable](https://img.shields.io/badge/stability-stable-green.svg) |
93
+ | [`@elenajs/bundler`](https://github.com/getelena/elena/tree/main/packages/bundler) | Elena bundler for component libraries. | [![npm](https://img.shields.io/npm/v/@elenajs/bundler.svg)](https://www.npmjs.com/package/@elenajs/bundler) | ![stability-stable](https://img.shields.io/badge/stability-stable-green.svg) |
94
+ | [`@elenajs/cli`](https://github.com/getelena/elena/tree/main/packages/cli) | Elena CLI for scaffolding web components. | [![npm](https://img.shields.io/npm/v/@elenajs/cli.svg)](https://www.npmjs.com/package/@elenajs/cli) | ![stability-stable](https://img.shields.io/badge/stability-stable-green.svg) |
95
+ | [`@elenajs/ssr`](https://github.com/getelena/elena/tree/main/packages/ssr) | Elena server-side rendering tools. | [![npm](https://img.shields.io/npm/v/@elenajs/ssr.svg)](https://www.npmjs.com/package/@elenajs/ssr) | ![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg) |
96
+ | [`@elenajs/mcp`](https://github.com/getelena/elena/tree/main/packages/ssr) | Elena MCP server. | [![npm](https://img.shields.io/npm/v/@elenajs/mcp.svg)](https://www.npmjs.com/package/@elenajs/mcp) | ![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg) |
97
+
98
+ <!-- https://github.com/orangemug/stability-badges -->
99
+
100
+ ## License
64
101
 
65
- Copyright © 2026 [Ariel Salminen](https://arielsalminen.com)
102
+ Released under the MIT License. Copyright © 2025-2026 [Ariel Salminen](https://arielsalminen.com).
package/dist/bundle.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @elenajs/core v1.0.0-rc.8
2
+ * @elenajs/core v1.0.0
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;if(!this.P&&!this.shadowRoot){const s={mode:t.shadow};t.registry&&(s.customElementRegistry=t.registry),this.P=this.attachShadow(s)}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(t){const s=this.tagName;s?function(t,s,i){const n=i??globalThis.customElements;n?.get(t)||n?.define(t,s)}(s,this,t):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
@@ -4,7 +4,7 @@
4
4
  * @param {string} tagName
5
5
  * @param {Function} Element
6
6
  */
7
- export function defineElement(tagName: string, Element: Function): void;
7
+ export function defineElement(tagName: string, Element: Function, registry: any): void;
8
8
  export function escapeHtml(str: any): string;
9
9
  /**
10
10
  * Resolve an interpolated template value to its
@@ -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,0CAMhB;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,19 +13,23 @@ 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;
25
29
  reflect?: boolean;
26
30
  };
27
31
  export type ElenaElementConstructor = (new (...args: any[]) => HTMLElement & ElenaInstanceMembers) & {
28
- define(): void;
32
+ define(registry?: CustomElementRegistry): void;
29
33
  readonly observedAttributes: string[];
30
34
  tagName?: string;
31
35
  props?: (string | ElenaPropObject)[];
@@ -33,6 +37,7 @@ export type ElenaElementConstructor = (new (...args: any[]) => HTMLElement & Ele
33
37
  element?: string;
34
38
  shadow?: "open" | "closed";
35
39
  styles?: CSSStyleSheet | string | (CSSStyleSheet | string)[];
40
+ registry?: CustomElementRegistry;
36
41
  };
37
42
  import { html } from "./common/utils.js";
38
43
  import { unsafeHTML } from "./common/utils.js";
@@ -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":"AAmEA;;;;;;;;;GASG;AACH,kCAHW,gBAAgB,GACd,uBAAuB,CAgfnC;+BArhBY,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,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACnD,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;IAC7D,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC;qBA7CmE,mBAAmB;2BAAnB,mBAAmB;wBAAnB,mBAAmB"}
package/dist/elena.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @elenajs/core v1.0.0-rc.8
2
+ * @elenajs/core v1.0.0
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;if(!this._shadow&&!this.shadowRoot){const e={mode:t.shadow};t.registry&&(e.customElementRegistry=t.registry),this._shadow=this.attachShadow(e)}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(t){const e=this.tagName;e?o(e,this,t):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,r){const s=r??globalThis.customElements;s?.get(t)||s?.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,9 +1,21 @@
1
1
  {
2
2
  "name": "@elenajs/core",
3
- "version": "1.0.0-rc.8",
3
+ "version": "1.0.0",
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/",
7
+ "keywords": [
8
+ "progressive web components",
9
+ "custom elements manifest",
10
+ "progressive enhancement",
11
+ "custom elements",
12
+ "web components",
13
+ "design system",
14
+ "javascript",
15
+ "typescript",
16
+ "elena",
17
+ "ssr"
18
+ ],
7
19
  "repository": {
8
20
  "type": "git",
9
21
  "url": "git+https://github.com/getelena/elena.git",
@@ -43,18 +55,18 @@
43
55
  "clean": "rm -rf dist/"
44
56
  },
45
57
  "devDependencies": {
46
- "@playwright/test": "1.58.2",
58
+ "@playwright/test": "1.59.1",
47
59
  "@rollup/plugin-terser": "1.0.0",
48
- "@vitest/browser": "4.1.0",
49
- "@vitest/browser-playwright": "4.1.0",
50
- "@vitest/coverage-v8": "4.1.0",
51
- "happy-dom": "20.8.4",
60
+ "@vitest/browser": "4.1.2",
61
+ "@vitest/browser-playwright": "4.1.2",
62
+ "@vitest/coverage-v8": "4.1.2",
63
+ "happy-dom": "20.8.9",
52
64
  "lit": "3.3.2",
53
- "rollup": "4.59.0",
65
+ "rollup": "4.60.1",
54
66
  "rollup-plugin-summary": "3.0.1",
55
67
  "serve": "14.2.6",
56
- "typescript": "5.9.3",
57
- "vitest": "4.1.0"
68
+ "typescript": "6.0.2",
69
+ "vitest": "4.1.2"
58
70
  },
59
- "gitHead": "1ed3381ee856296e0fa92ff863447843e8a51957"
71
+ "gitHead": "ef466853f84deb6559ea66b54cd85b7e043e9f36"
60
72
  }
@@ -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
@@ -14,9 +15,9 @@ export { prefix, isArray };
14
15
  * @param {string} tagName
15
16
  * @param {Function} Element
16
17
  */
17
- export function defineElement(tagName, Element) {
18
- const customElements = globalThis.customElements;
19
- customElements?.get(tagName) || customElements?.define(tagName, Element);
18
+ export function defineElement(tagName, Element, registry) {
19
+ const reg = registry ?? globalThis.customElements;
20
+ reg?.get(tagName) || reg?.define(tagName, Element);
20
21
  }
21
22
 
22
23
  /**
@@ -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
  /**
@@ -49,7 +49,7 @@ function elementResolver(selector) {
49
49
 
50
50
  /**
51
51
  * @typedef {(new (...args: any[]) => HTMLElement & ElenaInstanceMembers) & {
52
- * define(): void,
52
+ * define(registry?: CustomElementRegistry): void,
53
53
  * readonly observedAttributes: string[],
54
54
  * tagName?: string,
55
55
  * props?: (string | ElenaPropObject)[],
@@ -57,6 +57,7 @@ function elementResolver(selector) {
57
57
  * element?: string,
58
58
  * shadow?: "open" | "closed",
59
59
  * styles?: CSSStyleSheet | string | (CSSStyleSheet | string)[],
60
+ * registry?: CustomElementRegistry,
60
61
  * }} ElenaElementConstructor
61
62
  */
62
63
 
@@ -91,8 +92,8 @@ export function Elena(superClass) {
91
92
  * Updates the matching prop and re-renders if needed.
92
93
  *
93
94
  * @param {string} prop
94
- * @param {string} oldValue
95
- * @param {string} newValue
95
+ * @param {string | null} oldValue
96
+ * @param {string | null} newValue
96
97
  */
97
98
  attributeChangedCallback(prop, oldValue, newValue) {
98
99
  super.attributeChangedCallback?.(prop, oldValue, newValue);
@@ -102,17 +103,29 @@ export function Elena(superClass) {
102
103
  return;
103
104
  }
104
105
 
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;
106
+ if (oldValue === newValue) {
107
+ return;
108
+ }
110
109
 
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) {
110
+ if (this._hydrated && !this._isRendering) {
111
+ // The attribute is already set and we just need the coerced
112
+ // prop value stored for the next render.
113
+ const current = this._props.get(prop);
114
+ const type = typeof current;
115
+ const coerced =
116
+ type === "string" ? (newValue ?? "") : getPropValue(type, newValue, "toProp");
117
+
118
+ if (coerced !== current) {
119
+ this._props.set(prop, coerced);
120
+ }
115
121
  this._safeRender();
122
+
123
+ // Runs pre-hydration or during render.
124
+ // Goes through the setter so _props is initialized correctly.
125
+ } else {
126
+ this._syncing = true;
127
+ getProps(this, prop, oldValue, newValue);
128
+ this._syncing = false;
116
129
  }
117
130
  }
118
131
 
@@ -141,6 +154,16 @@ export function Elena(superClass) {
141
154
  this.text = this.textContent.trim();
142
155
  }
143
156
  this._attachShadow();
157
+ this._root = this._shadow ?? this.shadowRoot ?? this;
158
+
159
+ this._runUpdate ??= () => {
160
+ try {
161
+ this._performUpdate();
162
+ } catch (e) {
163
+ console.error(prefix, e);
164
+ }
165
+ };
166
+
144
167
  this.willUpdate();
145
168
  this._applyRender();
146
169
  this._syncProps();
@@ -228,16 +251,6 @@ export function Elena(superClass) {
228
251
  this._syncing = false;
229
252
  }
230
253
 
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
254
  /**
242
255
  * Attaches a shadow root and adopts styles on first connect.
243
256
  * Only runs when `static shadow` is set on the component class.
@@ -255,7 +268,11 @@ export function Elena(superClass) {
255
268
  // In that case skip attachShadow() but still adopt styles below.
256
269
  // Store the reference so closed shadow roots remain accessible.
257
270
  if (!this._shadow && !this.shadowRoot) {
258
- this._shadow = this.attachShadow({ mode: component.shadow });
271
+ const options = { mode: component.shadow };
272
+ if (component.registry) {
273
+ options.customElementRegistry = component.registry;
274
+ }
275
+ this._shadow = this.attachShadow(options);
259
276
  }
260
277
 
261
278
  const shadowRoot = this._shadow ?? this.shadowRoot;
@@ -290,14 +307,14 @@ export function Elena(superClass) {
290
307
  */
291
308
  _applyRender() {
292
309
  const constructor = this.constructor;
293
- const root = this._renderRoot;
310
+ const root = this._root;
294
311
  const result = this.render();
295
312
 
296
313
  if (result && result.strings) {
297
314
  const rebuilt = renderTemplate(root, result.strings, result.values);
298
315
 
299
316
  // Re-resolve element ref when the DOM was fully rebuilt.
300
- // Fast-path text node patching leaves the DOM structure intact,
317
+ // patch() and morph() leave the DOM structure intact,
301
318
  // so the existing ref is still valid.
302
319
  if (rebuilt) {
303
320
  const oldElement = this.element;
@@ -430,6 +447,7 @@ export function Elena(superClass) {
430
447
  * events in Shadow DOM (change, submit, reset).
431
448
  * Composed bubbling events (click, input) pass through on their own.
432
449
  *
450
+ * @param {Event} event
433
451
  * @internal
434
452
  */
435
453
  handleEvent(event) {
@@ -437,7 +455,7 @@ export function Elena(superClass) {
437
455
  return;
438
456
  }
439
457
 
440
- if (!event.bubbles || (!event.composed && this._renderRoot !== this)) {
458
+ if (!event.bubbles || (!event.composed && this._root !== this)) {
441
459
  /** @internal */
442
460
  this.dispatchEvent(new Event(event.type, { bubbles: event.bubbles }));
443
461
  }
@@ -467,11 +485,14 @@ export function Elena(superClass) {
467
485
  * Registers the component as a custom element using `static tagName`.
468
486
  * Call this on your component class after the class body is defined,
469
487
  * not on the Elena mixin itself.
488
+ *
489
+ * @param {CustomElementRegistry} [registry] - A scoped registry to register in.
490
+ * When omitted, registers in the global `customElements` registry.
470
491
  */
471
- static define() {
492
+ static define(registry) {
472
493
  const tag = this.tagName;
473
494
  if (tag) {
474
- defineElement(tag, this);
495
+ defineElement(tag, this, registry);
475
496
  } else {
476
497
  warn("define() without a tagName.");
477
498
  }
@@ -489,16 +510,7 @@ export function Elena(superClass) {
489
510
  }
490
511
  if (!this._renderPending) {
491
512
  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
- });
513
+ queueMicrotask(this._runUpdate);
502
514
  }
503
515
  }
504
516
 
@@ -523,7 +535,7 @@ export function Elena(superClass) {
523
535
  this.updated();
524
536
  } finally {
525
537
  this._updateComplete = null;
526
- resolve();
538
+ resolve?.();
527
539
  }
528
540
  }
529
541
 
@@ -534,7 +546,15 @@ export function Elena(superClass) {
534
546
  * @type {Promise<void>}
535
547
  */
536
548
  get updateComplete() {
537
- return this._updateComplete || Promise.resolve();
549
+ if (!this._renderPending) {
550
+ return Promise.resolve();
551
+ }
552
+ if (!this._updateComplete) {
553
+ this._updateComplete = new Promise(resolve => {
554
+ this._resolveUpdate = resolve;
555
+ });
556
+ }
557
+ return this._updateComplete;
538
558
  }
539
559
 
540
560
  /**