@symbiotejs/symbiote 3.5.8 → 3.6.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/AI_REFERENCE.md CHANGED
@@ -441,7 +441,7 @@ class MyList extends Symbiote {
441
441
  }
442
442
 
443
443
  MyList.template = html`
444
- <ul ${{itemize: 'items'}}>
444
+ <ul itemize="items">
445
445
  <template>
446
446
  <li>
447
447
  <span>{{name}}</span> - <span>{{role}}</span>
@@ -452,17 +452,41 @@ MyList.template = html`
452
452
  `;
453
453
  ```
454
454
 
455
- > **CRITICAL**: Inside itemize templates, items are full Symbiote components with their own state scope.
456
- > - `{{name}}`item's own property
457
- > - `${{onclick: 'handler'}}` — binds to the item component's own method/property
458
- > - `${{onclick: '^handler'}}` use `^` prefix to reach the **parent** component's property (must be in parent's `init$`)
459
- > - Failure to use `^` for parent handlers will result in broken event bindings
455
+ > **CRITICAL**: Inside itemize, each item is a Symbiote component with its own state scope.
456
+ > There are **two patterns** they determine how event handlers are bound:
457
+ >
458
+ > **Pattern 1: Inline `<template>` (dumb items)** items have no class definition, only data properties from the array.
459
+ > Any event handler or data must come from an **external context** not from the item itself.
460
+ > All context prefixes work: `^` (parent pop-up), `APP/` (named), `*` (shared). The most common is `^`:
461
+ > ```html
462
+ > <ul itemize="items">
463
+ > <template>
464
+ > <li>{{name}} <button ${{onclick: '^onItemClick'}}>Click</button></li>
465
+ > </template>
466
+ > </ul>
467
+ > ```
468
+ > Without a context prefix, the binding looks for the handler on the item itself — which doesn't have it, so it breaks.
469
+ >
470
+ > **Pattern 2: Custom `item-tag` (smart items)** — items are full components with their own class, templates, and methods.
471
+ > Handlers defined on the item component itself do **NOT** need `^`:
472
+ > ```html
473
+ > <div itemize="items" item-tag="my-item"></div>
474
+ > ```
475
+ > ```js
476
+ > class MyItem extends Symbiote {
477
+ > onItemClick() { console.log(this.$.name); }
478
+ > }
479
+ > MyItem.template = html`<li>{{name}} <button ${{onclick: 'onItemClick'}}>Click</button></li>`;
480
+ > MyItem.reg('my-item');
481
+ > ```
482
+ > Here `onItemClick` is the item's own method — no `^` needed.
483
+ > You can still use `^` to reach the parent list component if needed.
460
484
 
461
485
  ### Custom item component
462
486
  ```html
463
- <div ${{itemize: 'items', 'item-tag': 'my-item'}}></div>
487
+ <div itemize="items" item-tag="my-item"></div>
464
488
  ```
465
- Then define `my-item` as a separate Symbiote component.
489
+ Then define `my-item` as a separate Symbiote component with its own template, methods, and state.
466
490
 
467
491
  ### Data formats
468
492
  - **Array**: `[{prop: val}, ...]` — items rendered in order
@@ -799,7 +823,7 @@ import '@symbiotejs/symbiote/core/devMessages.js';
799
823
 
800
824
  1. **DON'T** use `this` in template strings — templates are decoupled from component context
801
825
  2. **DON'T** nest property keys with dots in state: `'obj.prop'` won't work as a state key
802
- 3. **DON'T** forget `^` prefix when referencing **parent** component properties from itemize items
826
+ 3. **DON'T** forget `^` prefix when referencing **parent** handlers from inline `<template>` itemize items (dumb items without their own class). Custom `item-tag` components with own methods bind directly without `^`
803
827
  4. **DON'T** use `@` prefix directly in HTML — it's only for binding syntax (`${{'@attr': 'prop'}}`)
804
828
  5. **DON'T** treat `init$` as a regular object — it's processed at connection time
805
829
  6. **DON'T** define `template` inside the class body (`static template = html\`...\`` won't work) — it's a static property **setter**, assign it outside: `MyComponent.template = html\`...\``. Same applies to `rootStyles` and `shadowStyles`.
package/README.md CHANGED
@@ -23,6 +23,7 @@ Symbiote.js gives you the convenience of a modern framework while staying close
23
23
  - **Dev mode** - `Symbiote.devMode` enables verbose warnings; import `devMessages.js` for full human-readable messages.
24
24
  - **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM.
25
25
  - **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods.
26
+ - **Lazy mode** - `lazyMode` flag defers component initialization and rendering based on viewport visibility. Can also be enabled via the `lazy` attribute on `itemize` containers to efficiently handle massive data sets.
26
27
  - And [more](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md).
27
28
 
28
29
  ## Quick start
@@ -184,6 +185,8 @@ TaskList.template = html`
184
185
 
185
186
  Items have their own state scope. Use the **`^` prefix** to reach higher-level component properties and handlers - `'^onItemClick'` binds to the parent's `onItemClick`, not the item's. Properties referenced via `^` must be defined in the parent's `init$`.
186
187
 
188
+ > **Performance Tip:** For massive lists, add the `lazy` attribute to the container (`<div itemize="tasks" lazy>`). It defers component initialization until they enter the viewport and cleans them up when they leave, heavily optimizing memory and rendering performance.
189
+
187
190
  ### Pop-up binding (`^`)
188
191
 
189
192
  The `^` prefix works in any nested component template - it walks up the DOM tree to find the nearest ancestor that has the property registered in its data context (`init$` or `add$()`):
package/core/Symbiote.js CHANGED
@@ -19,6 +19,9 @@ const trustedHTML = globalThis.trustedTypes ? trustedTypes.createPolicy('symbiot
19
19
 
20
20
  /** @template S */
21
21
  export class Symbiote extends HTMLElement {
22
+ /** @type {IntersectionObserver} */
23
+ static lazyObserver;
24
+
22
25
  /** @type {Boolean} */
23
26
  #initialized;
24
27
  /** @type {String} */
@@ -187,6 +190,8 @@ export class Symbiote extends HTMLElement {
187
190
  this.isVirtual = false;
188
191
  /** @type {Boolean} */
189
192
  this.allowTemplateInits = true;
193
+ /** @type {Boolean} */
194
+ this.lazyMode = false;
190
195
  }
191
196
 
192
197
  /** @returns {String} */
@@ -516,7 +521,52 @@ export class Symbiote extends HTMLElement {
516
521
  }
517
522
 
518
523
  connectedCallback() {
519
- this.#initComponent();
524
+ if (this.lazyMode) {
525
+ if (!Symbiote.lazyObserver) {
526
+ Symbiote.lazyObserver = new IntersectionObserver((entries) => {
527
+ entries.forEach((ent) => {
528
+ /** @type {any} */
529
+ let tgt = ent.target;
530
+ if (ent.isIntersecting) {
531
+ tgt.style.minHeight = '';
532
+ tgt.style.minWidth = '';
533
+ tgt.#initComponent();
534
+ } else {
535
+ tgt.#lazyDestroy();
536
+ }
537
+ });
538
+ });
539
+ }
540
+ Symbiote.lazyObserver.observe(this);
541
+ } else {
542
+ this.#initComponent();
543
+ }
544
+ }
545
+
546
+ #lazyDestroy() {
547
+ if (!this.connectedOnce) {
548
+ return;
549
+ }
550
+ let rect = this.getBoundingClientRect();
551
+ if (rect.height) {
552
+ this.style.minHeight = rect.height + 'px';
553
+ this.style.minWidth = rect.width + 'px';
554
+ }
555
+ if (this.shadowRoot) {
556
+ this.shadowRoot.innerHTML = '';
557
+ } else {
558
+ this.innerHTML = '';
559
+ }
560
+ for (let sub of this.allSubs) {
561
+ sub.remove();
562
+ this.allSubs.delete(sub);
563
+ }
564
+ this.#localCtx = null;
565
+ this.templateProcessors.clear();
566
+
567
+ this.connectedOnce = false;
568
+ this.#dataCtxInitialized = false;
569
+ this.#initialized = false;
520
570
  }
521
571
 
522
572
  destroyCallback() {}
@@ -532,6 +582,9 @@ export class Symbiote extends HTMLElement {
532
582
  destructionDelay = 100;
533
583
  disconnectedCallback() {
534
584
  if (globalThis.__SYMBIOTE_SSR) return;
585
+ if (this.lazyMode && Symbiote.lazyObserver) {
586
+ Symbiote.lazyObserver.unobserve(this);
587
+ }
535
588
  // if element wasn't connected, there is no need to disconnect it
536
589
  if (!this.connectedOnce) {
537
590
  return;
@@ -30,7 +30,7 @@ import { setupItemize } from './itemizeSetup.js';
30
30
  * @param {T} fnCtx
31
31
  */
32
32
  export function itemizeProcessor(fr, fnCtx) {
33
- setupItemize(fr, fnCtx, ({ el, itemClass, repeatDataKey, clientSSR }) => {
33
+ setupItemize(fr, fnCtx, ({ el, itemClass, repeatDataKey, clientSSR, isLazy }) => {
34
34
  /** @type {*[]} */
35
35
  let prevData = [];
36
36
 
@@ -88,6 +88,9 @@ export function itemizeProcessor(fr, fnCtx) {
88
88
  let fragment = document.createDocumentFragment();
89
89
  for (let i = prevLen; i < newLen; i++) {
90
90
  let repeatItem = new itemClass();
91
+ if (isLazy) {
92
+ repeatItem.lazyMode = true;
93
+ }
91
94
  Object.assign((repeatItem?.init$ || repeatItem), items[i]);
92
95
  fragment.appendChild(repeatItem);
93
96
  }
@@ -151,6 +154,9 @@ export function itemizeProcessor(fr, fnCtx) {
151
154
  newChildren.push(existing);
152
155
  } else {
153
156
  let repeatItem = new itemClass();
157
+ if (isLazy) {
158
+ repeatItem.lazyMode = true;
159
+ }
154
160
  Object.assign((repeatItem?.init$ || repeatItem), item);
155
161
  newChildren.push(repeatItem);
156
162
  }
@@ -193,6 +199,9 @@ export function itemizeProcessor(fr, fnCtx) {
193
199
  fragment = document.createDocumentFragment();
194
200
  }
195
201
  let repeatItem = new itemClass();
202
+ if (isLazy) {
203
+ repeatItem.lazyMode = true;
204
+ }
196
205
  Object.assign((repeatItem?.init$ || repeatItem), items[i]);
197
206
  fragment.appendChild(repeatItem);
198
207
  }
@@ -8,7 +8,7 @@ import { setupItemize } from './itemizeSetup.js';
8
8
  * @param {T} fnCtx
9
9
  */
10
10
  export function itemizeProcessor(fr, fnCtx) {
11
- setupItemize(fr, fnCtx, ({ el, itemClass, repeatDataKey, clientSSR }) => {
11
+ setupItemize(fr, fnCtx, ({ el, itemClass, repeatDataKey, clientSSR, isLazy }) => {
12
12
  fnCtx.sub(repeatDataKey, (data) => {
13
13
  if (!data) {
14
14
  while (el.firstChild) {
@@ -34,6 +34,9 @@ export function itemizeProcessor(fr, fnCtx) {
34
34
  fragment = document.createDocumentFragment();
35
35
  }
36
36
  let repeatItem = new itemClass();
37
+ if (isLazy) {
38
+ repeatItem.lazyMode = true;
39
+ }
37
40
  Object.assign((repeatItem?.init$ || repeatItem), item);
38
41
  fragment.appendChild(repeatItem);
39
42
  }
@@ -8,6 +8,7 @@ import { initPropFallback } from './initPropFallback.js';
8
8
  * @property {*} itemClass
9
9
  * @property {string} repeatDataKey
10
10
  * @property {boolean} clientSSR
11
+ * @property {boolean} isLazy
11
12
  */
12
13
 
13
14
  /**
@@ -78,7 +79,8 @@ export function setupItemize(fr, fnCtx, handler) {
78
79
  initPropFallback(fnCtx, repeatDataKey);
79
80
  }
80
81
 
81
- handler({ el, itemClass, repeatDataKey, clientSSR });
82
+ let isLazy = el.hasAttribute('lazy');
83
+ handler({ el, itemClass, repeatDataKey, clientSSR, isLazy });
82
84
 
83
85
  if (!globalThis.__SYMBIOTE_SSR) {
84
86
  el.removeAttribute(DICT.LIST_ATTR);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@symbiotejs/symbiote",
4
- "version": "3.5.8",
4
+ "version": "3.6.0",
5
5
  "description": "Symbiote.js - zero-dependency close-to-platform frontend library to build super-powered web components",
6
6
  "author": "team@rnd-pro.com",
7
7
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  export { html } from "./html.js";
2
2
  export { css } from "./css.js";
3
3
  export class Symbiote<S> extends HTMLElement {
4
+ static lazyObserver: IntersectionObserver;
4
5
  static __tpl: HTMLTemplateElement;
5
6
  static set devMode(val: boolean);
6
7
  static get devMode(): boolean;
@@ -38,6 +39,7 @@ export class Symbiote<S> extends HTMLElement {
38
39
  allowCustomTemplate: boolean;
39
40
  isVirtual: boolean;
40
41
  allowTemplateInits: boolean;
42
+ lazyMode: boolean;
41
43
  get cssCtxName(): string;
42
44
  get ctxName(): string;
43
45
  get localCtx(): PubSub<any>;
@@ -1 +1 @@
1
- {"version":3,"file":"Symbiote.d.ts","sourceRoot":"","sources":["../../core/Symbiote.js"],"names":[],"mappings":";;AAoBA,sBADc,CAAC;IAuBb,cADW,mBAAmB,CACjB;IAEb,iCAEC;IAED,8BAEC;IAkBD,wBAAgB;IA8JhB,+BAJwB,CAAC,SAAZ,aAAU,uBAEZ,CAAC;;;MA0CX;IAuQD,qCAA+B;IAqC/B,iDAFa,OAAO,QAAQ,CAkB3B;IAED,wBAKC;IAGD;;aAYC;IAmHD,6BADY,SAAS,aAAa,QAOjC;IAGD,+BADY,SAAS,aAAa,QAOjC;IAGD,8BADY,SAAS,aAAa,EAIjC;IAGD,gCADY,SAAS,aAAa,EAIjC;IA1kBD,cA6BC;IAzID,gCAEC;IAED,qBAAiB;IACjB,uBAAmB;IAiBnB,kBAHW,SAAS,gBAAgB,0BAuFnC;IAxDG,aAAiD;IA6DnD,OADW,CAAC,CACoB;IAEhC;;MAAmC;IAEnC,oBADW,GAAG,CAAC,CAAC,EAAE,EAAE,gBAAgB,gBAAW,EAAE,KAAK,eAAU,KAAK,IAAI,CAAC,CACvC;IAEnC;;MAA8B;IAC9B,kBAAwB;IAExB,qBAAwB;IAExB,sBAAyB;IAEzB,wBAA0B;IAE1B,0BAA6B;IAI7B,iBAAoB;IAEpB,6BAAgC;IAEhC,mBAAsB;IAEtB,4BAA8B;IAIhC,yBAEC;IAGD,sBASC;IAGD,4BAKC;IAGD,6BAEC;IAuDD,IALuB,CAAC,SAAX,MAAO,CAAE,QACX,CAAC,WACD,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,wBAuC/B;IAGD,2BAIC;IAGD,uBAIC;IAQD,IALuB,CAAC,SAAX,MAAO,CAAE,qBAEX,CAAC,CAAC,CAAC,CAAC,2BAOd;IAMD,UAHW,OAAO,CAAC,CAAC,CAAC,2BAOpB;IAGD,SADc,CAAC,CA6Bd;IAMD,YAHW,OAAO,CAAC,CAAC,CAAC,mCAcpB;IAED,8BAgBC;IAdG,4CASE;IAmFF,0BAAwC;IAwB1C,uBAAyB;IAG3B,0BAEC;IAED,wBAAoB;IAUpB,yBAAuB;IACvB,6BA2BC;IA+CD,oEAeC;IAMD,yDAoBC;IAYD,0BAME;IAMF,0CAFW,GAAG,QAgBb;IAED,yBAGC;IAOD,8EAmBC;;CA+BF;;mBA3uBkB,aAAa;qBAEX,iBAAiB;2BACX,iBAAiB"}
1
+ {"version":3,"file":"Symbiote.d.ts","sourceRoot":"","sources":["../../core/Symbiote.js"],"names":[],"mappings":";;AAoBA,sBADc,CAAC;IAGb,qBADW,oBAAoB,CACX;IAuBpB,cADW,mBAAmB,CACjB;IAEb,iCAEC;IAED,8BAEC;IAkBD,wBAAgB;IAgKhB,+BAJwB,CAAC,SAAZ,aAAU,uBAEZ,CAAC;;;MA0CX;IAoTD,qCAA+B;IAwC/B,iDAFa,OAAO,QAAQ,CAkB3B;IAED,wBAKC;IAGD;;aAYC;IAmHD,6BADY,SAAS,aAAa,QAOjC;IAGD,+BADY,SAAS,aAAa,QAOjC;IAGD,8BADY,SAAS,aAAa,EAIjC;IAGD,gCADY,SAAS,aAAa,EAIjC;IA5nBD,cA+BC;IA3ID,gCAEC;IAED,qBAAiB;IACjB,uBAAmB;IAiBnB,kBAHW,SAAS,gBAAgB,0BAuFnC;IAxDG,aAAiD;IA6DnD,OADW,CAAC,CACoB;IAEhC;;MAAmC;IAEnC,oBADW,GAAG,CAAC,CAAC,EAAE,EAAE,gBAAgB,gBAAW,EAAE,KAAK,eAAU,KAAK,IAAI,CAAC,CACvC;IAEnC;;MAA8B;IAC9B,kBAAwB;IAExB,qBAAwB;IAExB,sBAAyB;IAEzB,wBAA0B;IAE1B,0BAA6B;IAI7B,iBAAoB;IAEpB,6BAAgC;IAEhC,mBAAsB;IAEtB,4BAA8B;IAE9B,kBAAqB;IAIvB,yBAEC;IAGD,sBASC;IAGD,4BAKC;IAGD,6BAEC;IAuDD,IALuB,CAAC,SAAX,MAAO,CAAE,QACX,CAAC,WACD,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,wBAuC/B;IAGD,2BAIC;IAGD,uBAIC;IAQD,IALuB,CAAC,SAAX,MAAO,CAAE,qBAEX,CAAC,CAAC,CAAC,CAAC,2BAOd;IAMD,UAHW,OAAO,CAAC,CAAC,CAAC,2BAOpB;IAGD,SADc,CAAC,CA6Bd;IAMD,YAHW,OAAO,CAAC,CAAC,CAAC,mCAcpB;IAED,8BAgBC;IAdG,4CASE;IAmFF,0BAAwC;IAwB1C,uBAAyB;IAG3B,0BAqBC;IA4BD,wBAAoB;IAUpB,yBAAuB;IACvB,6BA8BC;IA+CD,oEAeC;IAMD,yDAoBC;IAYD,0BAME;IAMF,0CAFW,GAAG,QAgBb;IAED,yBAGC;IAOD,8EAmBC;;CA+BF;;mBAhyBkB,aAAa;qBAEX,iBAAiB;2BACX,iBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"itemizeProcessor-keyed.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor-keyed.js"],"names":[],"mappings":"AA+BA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QAkLX"}
1
+ {"version":3,"file":"itemizeProcessor-keyed.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor-keyed.js"],"names":[],"mappings":"AA+BA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QA2LX"}
@@ -1 +1 @@
1
- {"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"AASA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QAyDX"}
1
+ {"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"AASA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QA4DX"}
@@ -4,5 +4,6 @@ export type ItemizeDescriptor = {
4
4
  itemClass: any;
5
5
  repeatDataKey: string;
6
6
  clientSSR: boolean;
7
+ isLazy: boolean;
7
8
  };
8
9
  //# sourceMappingURL=itemizeSetup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"itemizeSetup.d.ts","sourceRoot":"","sources":["../../core/itemizeSetup.js"],"names":[],"mappings":"AAsBA,6BALgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,WACD,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,QAmE3C;;QAjFa,OAAO;eACP,GAAC;mBACD,MAAM;eACN,OAAO"}
1
+ {"version":3,"file":"itemizeSetup.d.ts","sourceRoot":"","sources":["../../core/itemizeSetup.js"],"names":[],"mappings":"AAuBA,6BALgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,WACD,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,QAoE3C;;QAnFa,OAAO;eACP,GAAC;mBACD,MAAM;eACN,OAAO;YACP,OAAO"}