@symbiotejs/symbiote 3.0.5 → 3.1.2

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
@@ -1,7 +1,7 @@
1
1
  # Symbiote.js — AI Context Reference (v3.x)
2
2
 
3
3
  > **Purpose**: Authoritative reference for AI code assistants. All information is derived from source code analysis of [symbiote.js](https://github.com/symbiotejs/symbiote.js).
4
- > Current version: **3.0.0-rc.1**. Zero dependencies. ~6 KB gzip.
4
+ > Current version: **3.1.0**. Zero dependencies. ~6 KB gzip.
5
5
 
6
6
  ---
7
7
 
@@ -33,25 +33,13 @@ Symbiote extends `HTMLElement`. Every component is a Custom Element.
33
33
 
34
34
  ```js
35
35
  class MyComponent extends Symbiote {
36
- // Initial reactive state (key-value pairs)
37
- init$ = {
38
- name: 'World',
39
- count: 0,
40
- onBtnClick: () => {
41
- this.$.count++;
42
- },
43
- };
44
-
45
- // Called once after init$ is processed but BEFORE template is rendered
46
- initCallback() {}
36
+ // Class properties as initial values (fallback)
37
+ name = 'World';
38
+ count = 0;
47
39
 
48
- // Called once AFTER template is rendered and attached to DOM
49
- renderCallback() {
50
- // Safe to access this.ref, this.$, DOM children here
40
+ onBtnClick() {
41
+ this.$.count++;
51
42
  }
52
-
53
- // Called when element is disconnected and readyToDestroy is true
54
- destroyCallback() {}
55
43
  }
56
44
 
57
45
  // Template — assigned via static property SETTER, outside the class body
@@ -70,6 +58,13 @@ MyComponent.reg('my-component');
70
58
  > Using `static template = html\`...\`` inside the class declaration **will NOT work**.
71
59
  > Templates are plain HTML strings, NOT JSX. Use the `html` tagged template literal.
72
60
 
61
+ ### Lifecycle callbacks (override in subclass)
62
+ | Method | When called |
63
+ |--------|------------|
64
+ | `initCallback()` | Once, after state initialized, before render (if `pauseRender=true`) or normally after render |
65
+ | `renderCallback()` | Once, after template is rendered and attached to DOM |
66
+ | `destroyCallback()` | On disconnect, after 100ms delay, only if `readyToDestroy=true` |
67
+
73
68
  ### Usage in HTML
74
69
  ```html
75
70
  <my-component></my-component>
@@ -95,21 +90,24 @@ Binds `propName` from component state to the text content of a text node. Works
95
90
  ### Property binding (element's own properties)
96
91
  ```html
97
92
  <button ${{onclick: 'handlerName'}}>Click</button>
98
- <div ${{textContent: 'myProp'}}></div>
93
+ <div>{{myProp}}</div>
99
94
  ```
100
95
  The `${{key: 'value'}}` interpolation creates a `bind="key:value;"` attribute. Keys are DOM element property names. Values are component state property names (strings).
101
96
 
102
- **Event handler resolution (3.x):** For `on*` bindings, Symbiote first looks for the key in `init$` (reactive state). If not found, it falls back to a **class method** with the same name. Both approaches work:
97
+ **Class property fallback (3.x):** For any binding key not found in `init$`, Symbiote checks own instance properties first (`Object.hasOwn` safe from inherited `HTMLElement` collisions), then prototype methods. Functions are automatically `.bind()`-ed to the component instance:
103
98
  ```js
104
99
  class MyComp extends Symbiote {
105
- // Approach 1: state property (arrow function)
106
- init$ = { onClick: () => console.log('clicked') };
100
+ // Approach 1: state property in init$
101
+ init$ = { count: 0 };
107
102
 
108
- // Approach 2: class method (fallback)
103
+ // Approach 2: class property / method (fallback)
104
+ label = 'Click me';
109
105
  onSubmit() { console.log('submitted'); }
110
106
  }
111
107
  ```
112
108
 
109
+ > **Recommendation:** Use class property fallback for **simple components** — keeps code compact. For **complex components** with many reactive properties, prefer `init$` to explicitly separate reactive state from regular class properties.
110
+
113
111
  ### Nested property binding
114
112
  ```html
115
113
  <div ${{'style.color': 'colorProp'}}>Text</div>
@@ -155,16 +153,16 @@ Prefixes control which data context a binding resolves to:
155
153
  | `^` | Parent inherited | `{{^parentProp}}` | Walk up DOM ancestry to find nearest component that has this prop |
156
154
  | `*` | Shared context | `{{*sharedProp}}` | Shared context scoped by `ctx` attribute or CSS `--ctx` |
157
155
  | `/` | Named context | `{{APP/myProp}}` | Global named context identified by key before `/` |
158
- | `--` | CSS Data | `${{textContent: '--my-css-var'}}` | Read value from CSS custom property |
156
+ | `--` | CSS Data | `{{--my-css-var}}` | Read value from CSS custom property |
159
157
  | `+` | Computed | (in init$) `'+sum': () => ...` | Function recalculated when local dependencies change (auto-tracked) |
160
158
 
161
159
  ### Examples in init$
162
160
  ```js
163
161
  init$ = {
164
- localProp: 'hello', // local
165
- '*sharedProp': 'shared value', // shared context
166
- 'APP/globalProp': 42, // named context "APP"
167
- '+computed': () => this.$.a + this.$.b, // local computed (auto-tracked)
162
+ localProp: 'hello', // local (prefer class properties for simple cases)
163
+ '*sharedProp': 'shared value', // shared context (requires init$)
164
+ 'APP/globalProp': 42, // named context (requires init$)
165
+ '+computed': () => this.$.a + this.$.b, // local computed (requires init$)
168
166
  };
169
167
  ```
170
168
 
@@ -277,18 +275,15 @@ Components grouped by the `ctx` HTML attribute (or `--ctx` CSS custom property)
277
275
 
278
276
  ```js
279
277
  class UploadBtn extends Symbiote {
280
- init$ = {
281
- '*files': [],
282
- onUpload: () => {
283
- this.$['*files'] = [...this.$['*files'], newFile];
284
- },
278
+ init$ = { '*files': [] }
279
+
280
+ onUpload() {
281
+ this.$['*files'] = [...this.$['*files'], newFile];
285
282
  }
286
283
  }
287
284
 
288
285
  class FileList extends Symbiote {
289
- init$ = {
290
- '*files': [], // same shared prop — first-registered value wins
291
- }
286
+ init$ = { '*files': [] } // same shared prop — first-registered value wins
292
287
  }
293
288
  ```
294
289
 
@@ -434,10 +429,11 @@ class MyList extends Symbiote {
434
429
  { name: 'Alice', role: 'Admin' },
435
430
  { name: 'Bob', role: 'User' },
436
431
  ],
437
- onItemClick: (e) => {
438
- console.log('clicked');
439
- },
440
- };
432
+ }
433
+
434
+ onItemClick(e) {
435
+ console.log('clicked');
436
+ }
441
437
  }
442
438
 
443
439
  MyList.template = html`
@@ -710,7 +706,7 @@ class MyComponent extends Symbiote {
710
706
 
711
707
  Or in template:
712
708
  ```html
713
- <div ${{textContent: '--my-css-prop'}}>...</div>
709
+ <div>{{--my-css-prop}}</div>
714
710
  ```
715
711
 
716
712
  Update with: `this.updateCssData()` / `this.dropCssDataCache()`.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Changed
6
+
7
+ - **Class property fallback (generalized).**
8
+ Bindings not found in `init$` now fall back to own class properties (checked via `Object.hasOwn`), not just `on*` event handlers. Functions are auto-bound to the component instance. Inherited `HTMLElement` properties are never picked up.
9
+ ```js
10
+ class MyComp extends Symbiote {
11
+ label = 'Click me';
12
+ onSubmit() { console.log('submitted'); }
13
+ }
14
+ ```
15
+ Previously only `on*` handlers supported this fallback.
16
+
3
17
  ## 3.0.0
4
18
 
5
19
  ### ⚠️ Breaking Changes
@@ -128,9 +142,10 @@
128
142
  { pattern: '/settings', load: () => import('./pages/settings.js') }
129
143
  ```
130
144
 
131
- - **Event handler method fallback.**
132
- `on*` bindings fall back to class methods when no `init$` property found:
145
+ - **Class property fallback.**
146
+ Bindings not in `init$` fall back to own class properties/methods:
133
147
  ```js
148
+ label = 'Click me';
134
149
  onSubmit() { console.log('submitted'); }
135
150
  ```
136
151
 
package/README.md CHANGED
@@ -17,7 +17,7 @@ Symbiote.js gives you the convenience of a modern framework while staying close
17
17
  - **Exit animations** — `animateOut(el)` for CSS-driven exit transitions, integrated into itemize API.
18
18
  - **Dev mode** — `Symbiote.devMode` enables verbose warnings for unresolved bindings.
19
19
  - **DSD hydration** — `ssrMode` supports both light DOM and Declarative Shadow DOM.
20
- - **on-event-handlers fallback** — event handlers now able to be described as direct class methods.
20
+ - **Class property fallback** — binding keys not in `init$` fall back to own class properties/methods.
21
21
  - And [more](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md).
22
22
 
23
23
  ## Quick start
@@ -29,11 +29,9 @@ No install needed — run this directly in a browser:
29
29
  import Symbiote, { html } from 'https://esm.run/@symbiotejs/symbiote';
30
30
 
31
31
  class MyCounter extends Symbiote {
32
- init$ = {
33
- count: 0,
34
- increment: () => {
35
- this.$.count++;
36
- },
32
+ count = 0;
33
+ increment() {
34
+ this.$.count++;
37
35
  }
38
36
  }
39
37
 
@@ -116,12 +114,10 @@ http.createServer(async (req, res) => {
116
114
 
117
115
  ```js
118
116
  class TodoItem extends Symbiote {
119
- init$ = {
120
- text: '',
121
- done: false,
122
- toggle: () => {
123
- this.$.done = !this.$.done;
124
- },
117
+ text = '';
118
+ done = false;
119
+ toggle() {
120
+ this.$.done = !this.$.done;
125
121
  }
126
122
  }
127
123
 
@@ -153,7 +149,7 @@ export default html`
153
149
  ```
154
150
 
155
151
  The `html` function supports two interpolation modes:
156
- - **Object** → reactive binding: `${{textContent: 'myProp'}}`
152
+ - **Object** → reactive binding: `${{onclick: 'handler'}}`
157
153
  - **String/number** → native concatenation: `${pageTitle}`
158
154
 
159
155
  ### Itemize (lists)
@@ -167,9 +163,10 @@ class TaskList extends Symbiote {
167
163
  { name: 'Buy groceries' },
168
164
  { name: 'Write docs' },
169
165
  ],
170
- onItemClick: () => {
171
- console.log('clicked!');
172
- },
166
+ }
167
+
168
+ onItemClick() {
169
+ console.log('clicked!');
173
170
  }
174
171
  }
175
172
 
@@ -224,25 +221,19 @@ Inspired by native HTML `name` attributes — like how `<input name="group">` gr
224
221
 
225
222
  ```js
226
223
  class UploadBtn extends Symbiote {
227
- init$ = {
228
- '*files': [],
229
- onUpload: () => {
230
- ...
231
- this.$['*files'] = [...this.$['*files'], newFile];
232
- },
224
+ init$ = { '*files': [] }
225
+
226
+ onUpload() {
227
+ this.$['*files'] = [...this.$['*files'], newFile];
233
228
  }
234
229
  }
235
230
 
236
231
  class FileList extends Symbiote {
237
- init$ = {
238
- '*files': [],
239
- }
232
+ init$ = { '*files': [] }
240
233
  }
241
234
 
242
235
  class StatusBar extends Symbiote {
243
- init$ = {
244
- '*files': [],
245
- }
236
+ init$ = { '*files': [] }
246
237
  }
247
238
  ```
248
239
 
@@ -328,7 +319,7 @@ class MyWidget extends Symbiote {
328
319
  }
329
320
 
330
321
  MyWidget.template = html`
331
- <span ${{textContent: '--label'}}></span>
322
+ <span>{{--label}}</span>
332
323
  `;
333
324
  ```
334
325
 
@@ -32,6 +32,11 @@ export function itemizeProcessor(fr, fnCtx) {
32
32
  el.firstChild.remove();
33
33
  }
34
34
  let repeatDataKey = el.getAttribute(DICT.LIST_ATTR);
35
+ if (!fnCtx.has(repeatDataKey) && fnCtx.allowTemplateInits) {
36
+ if (Object.hasOwn(fnCtx, repeatDataKey) && fnCtx[repeatDataKey] !== undefined) {
37
+ fnCtx.add(repeatDataKey, fnCtx[repeatDataKey]);
38
+ }
39
+ }
35
40
  fnCtx.sub(repeatDataKey, (data) => {
36
41
  if (!data) {
37
42
  while (el.firstChild) {
@@ -60,10 +60,15 @@ function domBindProcessor(fr, fnCtx) {
60
60
  if (!fnCtx.has(valKey) && fnCtx.allowTemplateInits) {
61
61
  if (valKey.startsWith(DICT.ATTR_BIND_PX)) {
62
62
  fnCtx.add(valKey, fnCtx.getAttribute(valKey.replace(DICT.ATTR_BIND_PX, '')));
63
+ } else if (Object.hasOwn(fnCtx, valKey) && fnCtx[valKey] !== undefined) {
64
+ let ownVal = fnCtx[valKey];
65
+ fnCtx.add(valKey, typeof ownVal === 'function' ? ownVal.bind(fnCtx) : ownVal);
66
+ } else if (typeof fnCtx[valKey] === 'function') {
67
+ fnCtx.add(valKey, fnCtx[valKey].bind(fnCtx));
63
68
  } else {
64
69
  fnCtx.add(valKey, null);
65
70
  // Dev-only: warn about bindings that aren't in init$ (likely typos)
66
- if (fnCtx.Symbiote?.devMode && !prop.startsWith('on')) {
71
+ if (fnCtx.Symbiote?.devMode) {
67
72
  let known = Object.keys(fnCtx.init$).filter((k) => !k.startsWith('+'));
68
73
  console.warn(
69
74
  `[Symbiote dev] <${fnCtx.localName}>: binding key "${valKey}" not found in init$ (auto-initialized to null).\n`
@@ -72,10 +77,6 @@ function domBindProcessor(fr, fnCtx) {
72
77
  }
73
78
  }
74
79
  }
75
- // In case of event handler is null, bind to fallback method (if defined):
76
- if (prop.startsWith('on') && fnCtx.localCtx.read(valKey) === null && typeof fnCtx[valKey] === 'function') {
77
- fnCtx.add(valKey, fnCtx[valKey].bind(fnCtx), true);
78
- }
79
80
  fnCtx.sub(valKey, (val) => {
80
81
  if (castType === 'double') {
81
82
  val = !!val;
@@ -159,6 +160,11 @@ const txtNodesProcessor = function (fr, fnCtx) {
159
160
  if (prop.startsWith(DICT.ATTR_BIND_PX)) {
160
161
  fnCtx.add(prop, fnCtx.getAttribute(prop.replace(DICT.ATTR_BIND_PX, '')));
161
162
  fnCtx.initAttributeObserver();
163
+ } else if (Object.hasOwn(fnCtx, prop) && fnCtx[prop] !== undefined) {
164
+ let ownVal = fnCtx[prop];
165
+ fnCtx.add(prop, typeof ownVal === 'function' ? ownVal.bind(fnCtx) : ownVal);
166
+ } else if (typeof fnCtx[prop] === 'function') {
167
+ fnCtx.add(prop, fnCtx[prop].bind(fnCtx));
162
168
  } else {
163
169
  fnCtx.add(prop, null);
164
170
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@symbiotejs/symbiote",
4
- "version": "3.0.5",
4
+ "version": "3.1.2",
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 +1 @@
1
- {"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"AASA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QAoFX"}
1
+ {"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"AASA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QAyFX"}
@@ -1 +1 @@
1
- {"version":3,"file":"tpl-processors.d.ts","sourceRoot":"","sources":["../../core/tpl-processors.js"],"names":[],"mappings":"0BAgIgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC"}
1
+ {"version":3,"file":"tpl-processors.d.ts","sourceRoot":"","sources":["../../core/tpl-processors.js"],"names":[],"mappings":"0BAiIgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC"}