@symbiotejs/symbiote 3.8.1 → 3.8.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/README.md +19 -106
- package/docs/README.md +0 -6
- package/docs/animations.md +1 -1
- package/docs/attributes.md +2 -2
- package/docs/common-mistakes.md +63 -1
- package/docs/context.md +212 -73
- package/docs/ecosystem.md +0 -3
- package/docs/llms-index.md +0 -1
- package/llms-full.txt +297 -183
- package/llms.txt +0 -1
- package/package.json +1 -2
- package/scripts/build-llms.js +0 -2
- package/CHANGELOG.md +0 -372
- package/docs/lit-vs-symbiote.md +0 -200
- package/docs/migration-2x-to-3x.md +0 -171
package/llms-full.txt
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no black boxes, no excess repaints. No build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional.
|
|
6
6
|
|
|
7
|
-
Symbiote.js gives you the convenience of a modern framework while staying close to the native platform - HTML, CSS, and DOM APIs. Components are custom elements that work everywhere: in any framework, in plain HTML, in a micro-frontend architecture. And with **isomorphic mode**, the same component code works on the server and the client - server-rendered pages hydrate automatically, no diffing, no mismatch errors.
|
|
8
|
-
|
|
9
7
|
Here are the three most important differences between Symbiote.js and other frameworks:
|
|
10
|
-
1. Natural DOM Extension Philosophy - designed to extend platform, not to replace
|
|
11
|
-
2. Runtime-Agnostic HTML Templates - outstanding flexibility for rendering strategies and further customization
|
|
12
|
-
3. Powerful App-wide State Management - combine data contexts without bloated boilerplate or external tools
|
|
8
|
+
1. **Natural DOM Extension Philosophy** - designed to extend platform APIs, not to replace them
|
|
9
|
+
2. **Runtime-Agnostic HTML Templates** - outstanding flexibility for rendering strategies and further customization
|
|
10
|
+
3. **Powerful App-wide State Management** - combine data contexts without bloated boilerplate or external tools
|
|
13
11
|
|
|
14
12
|
## What's new in v3.x?
|
|
15
13
|
|
|
@@ -23,7 +21,6 @@ Here are the three most important differences between Symbiote.js and other fram
|
|
|
23
21
|
- **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM.
|
|
24
22
|
- **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods.
|
|
25
23
|
- **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
|
-
- And [more](https://github.com/symbiotejs/symbiote.js/blob/webmcp/CHANGELOG.md).
|
|
27
24
|
|
|
28
25
|
## Quick start
|
|
29
26
|
|
|
@@ -61,57 +58,6 @@ npm i @symbiotejs/symbiote
|
|
|
61
58
|
import Symbiote, { html, css } from '@symbiotejs/symbiote';
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
## Isomorphic Web Components
|
|
65
|
-
|
|
66
|
-
One component. Server-rendered or client-rendered - automatically. Set `isoMode = true` and the component figures it out: if server-rendered content exists, it hydrates; otherwise it renders from template. No conditional logic, no separate server/client versions:
|
|
67
|
-
```js
|
|
68
|
-
class MyComponent extends Symbiote {
|
|
69
|
-
isoMode = true;
|
|
70
|
-
count = 0;
|
|
71
|
-
increment() {
|
|
72
|
-
this.$.count++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
MyComponent.template = html`
|
|
77
|
-
<h2 ${{textContent: 'count'}}></h2>
|
|
78
|
-
<button ${{onclick: 'increment'}}>Click me!</button>
|
|
79
|
-
`;
|
|
80
|
-
MyComponent.reg('my-component');
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
This exact code runs **everywhere** - SSR on the server, hydration on the client, or pure client rendering. No framework split, no `'use client'` directives, no hydration mismatch errors.
|
|
84
|
-
|
|
85
|
-
### SSR - one class, zero config
|
|
86
|
-
|
|
87
|
-
Server rendering doesn't need a virtual DOM, a reconciler, or framework-specific packages:
|
|
88
|
-
|
|
89
|
-
```js
|
|
90
|
-
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
91
|
-
|
|
92
|
-
await SSR.init(); // patches globals with linkedom
|
|
93
|
-
await import('./my-app.js'); // components register normally
|
|
94
|
-
|
|
95
|
-
let html = await SSR.processHtml('<my-app></my-app>');
|
|
96
|
-
SSR.destroy();
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
For large pages, stream HTML chunks with `SSR.renderToStream()` for faster TTFB. See [SSR docs](./docs/ssr.md) and [server setup recipes](./docs/ssr-server.md).
|
|
100
|
-
|
|
101
|
-
### How it compares
|
|
102
|
-
|
|
103
|
-
| | **Symbiote.js** | **Next.js (React)** | **Lit** (`@lit-labs/ssr`) |
|
|
104
|
-
|--|----------------|---------------------|----|
|
|
105
|
-
| **Isomorphic code** | Same code, `isoMode` auto-detects | Server Components vs Client Components split | Same code, but load-order constraints |
|
|
106
|
-
| **Hydration** | Binding-based - attaches to existing DOM, no diffing | `hydrateRoot()` - must produce identical output or errors | Requires `ssr-client` + hydrate support module |
|
|
107
|
-
| **Packages** | 1 module + `linkedom` peer dep | Full framework buy-in | 3 packages: `ssr`, `ssr-client`, `ssr-dom-shim` |
|
|
108
|
-
| **Streaming** | `renderToStream()` async generator | `renderToPipeableStream()` | Iterable `RenderResult` |
|
|
109
|
-
| **Mismatch handling** | Not needed - bindings attach to whatever DOM exists | Hard errors if server/client output differs | N/A |
|
|
110
|
-
| **Template output** | Clean HTML with `bind=` attributes | HTML with framework markers | HTML with `<!--lit-part-->` comment markers |
|
|
111
|
-
| **Lock-in** | None - standard Web Components | Full framework commitment | Lit-specific, but Web Components |
|
|
112
|
-
|
|
113
|
-
**Key insight:** There are no hydration mismatches because there's no diffing. The server produces HTML with binding attributes. The client reads those attributes and adds reactivity. That's it.
|
|
114
|
-
|
|
115
61
|
## Core concepts
|
|
116
62
|
|
|
117
63
|
### Reactive state
|
|
@@ -140,7 +86,7 @@ This makes it easy to control Symbiote-based widgets and microfrontends from any
|
|
|
140
86
|
|
|
141
87
|
### Templates
|
|
142
88
|
|
|
143
|
-
Templates are plain HTML strings -
|
|
89
|
+
Templates are plain HTML strings - runtime-agnostic, easy to test, easy to move between files:
|
|
144
90
|
|
|
145
91
|
```js
|
|
146
92
|
// Separate file: my-component.template.js
|
|
@@ -158,34 +104,24 @@ The `html` function supports two interpolation modes:
|
|
|
158
104
|
|
|
159
105
|
### Itemize (dynamic reactive lists)
|
|
160
106
|
|
|
161
|
-
Render lists from data arrays with efficient updates:
|
|
107
|
+
Render lists from data arrays or objects with efficient updates:
|
|
162
108
|
```js
|
|
163
109
|
class TaskList extends Symbiote {
|
|
164
110
|
tasks = [
|
|
165
111
|
{ name: 'Buy groceries' },
|
|
166
112
|
{ name: 'Write docs' },
|
|
167
113
|
];
|
|
168
|
-
init$ = {
|
|
169
|
-
// Needs to be defined in init$ for pop-up binding to work
|
|
170
|
-
onItemClick: () => {
|
|
171
|
-
console.log('clicked!');
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
114
|
}
|
|
175
115
|
|
|
176
116
|
TaskList.template = html`
|
|
177
|
-
<
|
|
117
|
+
<ul itemize="tasks">
|
|
178
118
|
<template>
|
|
179
|
-
<
|
|
119
|
+
<li>{{name}}</li>
|
|
180
120
|
</template>
|
|
181
|
-
</
|
|
121
|
+
</ul>
|
|
182
122
|
`;
|
|
183
123
|
```
|
|
184
124
|
|
|
185
|
-
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
|
-
> **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.
|
|
188
|
-
|
|
189
125
|
### Pop-up binding (`^`)
|
|
190
126
|
|
|
191
127
|
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$()`):
|
|
@@ -198,14 +134,11 @@ The `^` prefix works in any nested component template - it walks up the DOM tree
|
|
|
198
134
|
<button ${{onclick: '^parentHandler'}}>Click</button>
|
|
199
135
|
```
|
|
200
136
|
|
|
201
|
-
> **Note:** Class property fallbacks are not checked by the `^` walk - the parent must define the property in `init$`.
|
|
202
|
-
|
|
203
137
|
### Named data contexts
|
|
204
138
|
|
|
205
139
|
Share state across components without prop drilling:
|
|
206
|
-
|
|
207
140
|
```js
|
|
208
|
-
import { PubSub } from '@symbiotejs/symbiote';
|
|
141
|
+
import { PubSub, html } from '@symbiotejs/symbiote';
|
|
209
142
|
|
|
210
143
|
PubSub.registerCtx({
|
|
211
144
|
user: 'Alex',
|
|
@@ -214,6 +147,9 @@ PubSub.registerCtx({
|
|
|
214
147
|
|
|
215
148
|
// Any component can read/write:
|
|
216
149
|
this.$['APP/user'] = 'New name';
|
|
150
|
+
|
|
151
|
+
// Any template can use property directly:
|
|
152
|
+
let template = html`<h2>{{APP/user}}</h2>`;
|
|
217
153
|
```
|
|
218
154
|
|
|
219
155
|
### Shared context (`*`)
|
|
@@ -246,11 +182,10 @@ class StatusBar extends Symbiote {
|
|
|
246
182
|
|
|
247
183
|
All three components access the same `*files` state - no parent component, no prop drilling, no global store boilerplate. Just set `ctx="gallery"` in HTML and use `*`-prefixed properties. This makes it trivial to build complex component relationships purely in markup, with ready-made components that don't need to know about each other.
|
|
248
184
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
### Routing (optional module)
|
|
185
|
+
### Application routing
|
|
252
186
|
|
|
253
187
|
```js
|
|
188
|
+
// Import optional module:
|
|
254
189
|
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
255
190
|
|
|
256
191
|
AppRouter.initRoutingCtx('R', {
|
|
@@ -260,23 +195,7 @@ AppRouter.initRoutingCtx('R', {
|
|
|
260
195
|
});
|
|
261
196
|
```
|
|
262
197
|
|
|
263
|
-
###
|
|
264
|
-
|
|
265
|
-
CSS-driven transitions with zero JS animation code:
|
|
266
|
-
|
|
267
|
-
```css
|
|
268
|
-
task-item {
|
|
269
|
-
opacity: 1;
|
|
270
|
-
transition: opacity 0.3s;
|
|
271
|
-
|
|
272
|
-
@starting-style { opacity: 0; } /* enter */
|
|
273
|
-
&[leaving] { opacity: 0; } /* exit */
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
`animateOut(el)` sets `[leaving]`, waits for `transitionend`, then removes. Itemize uses this automatically.
|
|
278
|
-
|
|
279
|
-
### Styling
|
|
198
|
+
### CSS Styling
|
|
280
199
|
|
|
281
200
|
Shadow DOM is **optional** in Symbiote - use it when you need isolation, skip it when you don't. This gives full flexibility:
|
|
282
201
|
|
|
@@ -293,6 +212,8 @@ MyComponent.rootStyles = css`
|
|
|
293
212
|
`;
|
|
294
213
|
```
|
|
295
214
|
|
|
215
|
+
This style will be applied to nearest upper shadow root, if exists and to common document if not.
|
|
216
|
+
|
|
296
217
|
**Shadow DOM** - opt-in isolation when needed:
|
|
297
218
|
|
|
298
219
|
```js
|
|
@@ -306,9 +227,9 @@ Isolated.shadowStyles = css`
|
|
|
306
227
|
|
|
307
228
|
All native CSS features work as expected: CSS variables flow through shadow boundaries, `::part()` exposes internals, modern nesting, `@layer`, `@container` - no framework abstractions in the way. Mix light DOM and shadow DOM components freely in the same app.
|
|
308
229
|
|
|
309
|
-
### CSS Data
|
|
230
|
+
### CSS Data
|
|
310
231
|
|
|
311
|
-
Components can read CSS custom
|
|
232
|
+
Components can read CSS custom property values to initiate reactive state:
|
|
312
233
|
|
|
313
234
|
```css
|
|
314
235
|
my-widget {
|
|
@@ -324,8 +245,6 @@ MyWidget.template = html`
|
|
|
324
245
|
`;
|
|
325
246
|
```
|
|
326
247
|
|
|
327
|
-
CSS values are parsed automatically - quoted strings become strings, numbers become numbers. Call `this.updateCssData()` to re-read after runtime CSS changes. This enables CSS-driven configuration: theme values, layout parameters, or localized strings - all settable from CSS without touching JS.
|
|
328
|
-
|
|
329
248
|
## Best for
|
|
330
249
|
|
|
331
250
|
- **Complex widgets** embedded in any host application
|
|
@@ -336,19 +255,13 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
|
|
|
336
255
|
- **Framework-agnostic solutions** - one codebase, any context
|
|
337
256
|
- **Modern AI-first web** - expose the application state to WebMCP tools automatically
|
|
338
257
|
|
|
339
|
-
## Browser support
|
|
340
|
-
|
|
341
|
-
All modern browsers: Chrome, Firefox, Safari, Edge, Opera.
|
|
342
|
-
|
|
343
258
|
## Docs & Examples
|
|
344
259
|
|
|
345
260
|
- [Documentation](https://github.com/symbiotejs/symbiote.js/blob/main/docs/README.md)
|
|
346
|
-
- [Lit vs Symbiote.js](https://github.com/symbiotejs/symbiote.js/blob/main/docs/lit-vs-symbiote.md) - Side-by-side comparison
|
|
347
261
|
- [Live Examples](https://rnd-pro.com/symbiote/3x/examples/) - Interactive Code Playground
|
|
348
262
|
- [JSDA-Kit](https://github.com/rnd-pro/jsda-kit) - All-in-one companion tool: server, SSG, bundling, import maps, and native Symbiote.js SSR integration
|
|
349
263
|
- [AI / llms.txt](https://rnd-pro.com/symbiote/llms.txt) — index for AI tools
|
|
350
264
|
- [Full docs (single file)](https://rnd-pro.com/symbiote/llms-full.txt) — complete merged reference for AI context
|
|
351
|
-
- [Changelog](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md)
|
|
352
265
|
|
|
353
266
|
## Related articles
|
|
354
267
|
|
|
@@ -405,7 +318,7 @@ my-item {
|
|
|
405
318
|
|
|
406
319
|
## Itemize integration
|
|
407
320
|
|
|
408
|
-
|
|
321
|
+
Itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
|
|
409
322
|
```css
|
|
410
323
|
user-card {
|
|
411
324
|
opacity: 1;
|
|
@@ -449,7 +362,7 @@ class MyComponent extends Symbiote {}
|
|
|
449
362
|
MyComponent.template = html`<h1>{{@attribute-name}}</h1>`;
|
|
450
363
|
```
|
|
451
364
|
|
|
452
|
-
Then use it as an attribute in markup:
|
|
365
|
+
Then use it as an component's attribute in markup:
|
|
453
366
|
```html
|
|
454
367
|
<my-component attribute-name="attribute value"></my-component>
|
|
455
368
|
```
|
|
@@ -473,7 +386,7 @@ MyComponent.observedAttributes = [
|
|
|
473
386
|
|
|
474
387
|
## `bindAttributes()` static method
|
|
475
388
|
|
|
476
|
-
|
|
389
|
+
Reflect attribute values to property values:
|
|
477
390
|
```js
|
|
478
391
|
class MyComponent extends Symbiote {
|
|
479
392
|
|
|
@@ -754,7 +667,69 @@ Named external contexts (`CTX/prop`) are different: they are registered globally
|
|
|
754
667
|
|
|
755
668
|
---
|
|
756
669
|
|
|
757
|
-
## 10.
|
|
670
|
+
## 10. Using `{{prop}}` binding syntax inside tag definitions
|
|
671
|
+
|
|
672
|
+
`{{prop}}` is a **text node** binding — it only works inside element content. Any binding that targets a tag itself — attributes, DOM properties, event handlers, or CSS custom properties — must use `${{}}` or `bind=""` syntax inside the opening tag.
|
|
673
|
+
|
|
674
|
+
```html
|
|
675
|
+
<!-- WRONG — {{}} syntax does not work inside tag definitions -->
|
|
676
|
+
<div class="{{APP/theme}}">...</div>
|
|
677
|
+
<img src="{{imageUrl}}">
|
|
678
|
+
<button onclick="{{onAction}}">Go</button>
|
|
679
|
+
|
|
680
|
+
<!-- CORRECT — use ${{}} binding object inside the tag -->
|
|
681
|
+
<div ${{'@class': 'APP/theme'}}>...</div>
|
|
682
|
+
<img ${{'@src': 'imageUrl'}}>
|
|
683
|
+
<button ${{onclick: 'onAction'}}>Go</button>
|
|
684
|
+
<div ${{'style.color': 'colorProp'}}>...</div>
|
|
685
|
+
|
|
686
|
+
<!-- CORRECT — or use the bind attribute string syntax -->
|
|
687
|
+
<div bind="@class: APP/theme">...</div>
|
|
688
|
+
<img bind="@src: imageUrl">
|
|
689
|
+
<button bind="onclick: onAction">Go</button>
|
|
690
|
+
|
|
691
|
+
<!-- CORRECT — {{}} works fine in text node content -->
|
|
692
|
+
<h1>Hello, {{userName}}!</h1>
|
|
693
|
+
<p>Theme: {{APP/theme}}</p>
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## 11. Expecting two-way data binding
|
|
699
|
+
|
|
700
|
+
Symbiote.js bindings are **one-way by design** — from state to DOM. There is no `v-model`, `[(ngModel)]`, or `bind:value` equivalent. To react to user input, wire the event handler explicitly and write back to state yourself.
|
|
701
|
+
|
|
702
|
+
```js
|
|
703
|
+
// WRONG — expecting the binding to also update state on user input
|
|
704
|
+
MyComponent.template = html`
|
|
705
|
+
<input ${{value: 'query'}}>
|
|
706
|
+
`;
|
|
707
|
+
|
|
708
|
+
// CORRECT — handle the input event and write back to state
|
|
709
|
+
class MyComponent extends Symbiote {
|
|
710
|
+
init$ = { query: '' }
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
MyComponent.template = html`
|
|
714
|
+
<input
|
|
715
|
+
${{value: 'query', oninput: 'onInput'}}
|
|
716
|
+
>
|
|
717
|
+
`;
|
|
718
|
+
```
|
|
719
|
+
```js
|
|
720
|
+
class MyComponent extends Symbiote {
|
|
721
|
+
init$ = {
|
|
722
|
+
query: '',
|
|
723
|
+
onInput: (e) => { this.$.query = e.target.value; },
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
This is intentional — explicit event handling keeps data flow predictable and avoids the hidden side effects that two-way binding can introduce.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## 12. Treating `init$` as a plain object
|
|
758
733
|
|
|
759
734
|
`init$` is processed once at connection time to populate the component's reactive context. Mutating it after the fact has no effect. Use `this.$` or `add$()` for runtime changes.
|
|
760
735
|
|
|
@@ -780,39 +755,46 @@ class MyComponent extends Symbiote {
|
|
|
780
755
|
|
|
781
756
|
# Context
|
|
782
757
|
|
|
783
|
-
|
|
758
|
+
Context is the central concept in Symbiote.js. Rather than passing data through prop chains or maintaining a separate global store, Symbiote uses the DOM structure itself as the data flow graph. Every component can read from and write to multiple data sources - its **context** is the union of all the sources currently accessible to it.
|
|
759
|
+
|
|
760
|
+
There are seven context types, each addressed by a token prefix in property keys:
|
|
784
761
|
|
|
785
|
-
|
|
762
|
+
| Token | Context Type | Scope |
|
|
763
|
+
|-------|-------------|-------|
|
|
764
|
+
| _(none)_ | [Local](#local-context) | Component instance |
|
|
765
|
+
| `^` | [Pop-up](#pop-up-context) | Nearest ancestor that owns the property |
|
|
766
|
+
| `*` | [Shared](#shared-context) | All components sharing a `ctx` attribute |
|
|
767
|
+
| `name/` | [Named](#named-context) | Any component, anywhere |
|
|
768
|
+
| `--` | [CSS Data](#css-data-context) | Inherited from the CSS cascade |
|
|
769
|
+
| `@` | [Attribute](./attributes.md) | HTML attribute on the element |
|
|
770
|
+
| `+` | [Computed](./properties.md#computed-properties) | Derived, auto-recalculated value |
|
|
771
|
+
|
|
772
|
+
---
|
|
786
773
|
|
|
787
774
|
## Local context
|
|
788
775
|
|
|
789
|
-
Local context
|
|
776
|
+
Local context is the component's own reactive state, scoped to the instance. It works the same way as component state in other frameworks and is invisible to other components.
|
|
777
|
+
|
|
778
|
+
Define properties in `init$`:
|
|
790
779
|
```js
|
|
791
780
|
class MyComponent extends Symbiote {
|
|
792
|
-
|
|
793
781
|
init$ = {
|
|
794
782
|
myProperty: 'some value',
|
|
795
783
|
}
|
|
796
|
-
|
|
797
784
|
}
|
|
798
785
|
```
|
|
799
786
|
|
|
800
|
-
Use the `$` proxy to read and write values:
|
|
787
|
+
Use the `$` proxy to read and write values at runtime:
|
|
801
788
|
```js
|
|
802
789
|
class MyComponent extends Symbiote {
|
|
803
|
-
|
|
804
790
|
init$ = {
|
|
805
791
|
myProperty: 'some value',
|
|
806
792
|
}
|
|
807
793
|
|
|
808
794
|
renderCallback() {
|
|
809
|
-
// Read:
|
|
810
795
|
console.log(this.$.myProperty); // > 'some value'
|
|
811
|
-
|
|
812
|
-
// Write:
|
|
813
796
|
this.$.myProperty = 'new value';
|
|
814
797
|
}
|
|
815
|
-
|
|
816
798
|
}
|
|
817
799
|
|
|
818
800
|
MyComponent.template = html`
|
|
@@ -820,97 +802,143 @@ MyComponent.template = html`
|
|
|
820
802
|
`;
|
|
821
803
|
```
|
|
822
804
|
|
|
805
|
+
For simple components without shared or computed props, you can also declare properties as plain class fields - Symbiote picks them up automatically via class-field fallback:
|
|
806
|
+
```js
|
|
807
|
+
class MyComponent extends Symbiote {
|
|
808
|
+
count = 0;
|
|
809
|
+
label = 'Hello';
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
> Use `init$` when you need shared (`*`), computed (`+`), or attribute (`@`) props in the same declaration - those tokens are only recognized inside `init$`.
|
|
814
|
+
|
|
815
|
+
See [Properties →](./properties.md) for the full property API: `add$()`, `sub()`, `set$()`, computed props, and more.
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
823
819
|
## Named context
|
|
824
820
|
|
|
825
|
-
Named context is
|
|
821
|
+
Named context is a global, named data store created independently of any component. Any component can read from or write to it using the `CONTEXT_NAME/property` syntax - regardless of DOM position.
|
|
826
822
|
|
|
827
|
-
|
|
823
|
+
Use named context when data must be shared across unrelated parts of the application.
|
|
824
|
+
|
|
825
|
+
**Creating a named context:**
|
|
826
|
+
```js
|
|
827
|
+
import { PubSub } from '@symbiotejs/symbiote';
|
|
828
|
+
|
|
829
|
+
let appCtx = PubSub.registerCtx({
|
|
830
|
+
theme: 'light',
|
|
831
|
+
user: null,
|
|
832
|
+
}, 'APP');
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**Accessing it from a component:**
|
|
836
|
+
```js
|
|
837
|
+
class MyComponent extends Symbiote {
|
|
838
|
+
init$ = {
|
|
839
|
+
'APP/theme': 'light', // optional local fallback - used if the named context hasn't published yet
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
renderCallback() {
|
|
843
|
+
console.log(this.$['APP/theme']); // read
|
|
844
|
+
this.$['APP/theme'] = 'dark'; // write - updates the named context for all subscribers
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
MyComponent.template = html`
|
|
849
|
+
<div ${{'@class': 'APP/theme'}}>...</div>
|
|
850
|
+
`;
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
**Example - localization:**
|
|
828
854
|
```js
|
|
829
855
|
import Symbiote, { html, PubSub } from '@symbiotejs/symbiote';
|
|
830
856
|
|
|
831
|
-
|
|
832
|
-
let EN = {
|
|
857
|
+
let l10nCtx = PubSub.registerCtx({
|
|
833
858
|
users: 'Users',
|
|
834
859
|
comments: 'Comments',
|
|
835
860
|
likes: 'Likes',
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
// Create localization context:
|
|
839
|
-
let l10nCtx = PubSub.registerCtx(EN, 'L10N');
|
|
840
|
-
|
|
841
|
-
// Use localized strings in templates:
|
|
842
|
-
class MyComponent extends Symbiote { ... }
|
|
861
|
+
}, 'L10N');
|
|
843
862
|
|
|
844
863
|
MyComponent.template = html`
|
|
845
864
|
<div>{{L10N/users}} - {{numberOfUsers}}</div>
|
|
846
865
|
<div>{{L10N/comments}} - {{numberOfComments}}</div>
|
|
847
866
|
<div>{{L10N/likes}} - {{numberOfLikes}}</div>
|
|
848
867
|
`;
|
|
849
|
-
```
|
|
850
868
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
Switching language:
|
|
854
|
-
```js
|
|
855
|
-
let ES = {
|
|
869
|
+
// Switch language at any time - all subscribed components update instantly:
|
|
870
|
+
l10nCtx.multiPub({
|
|
856
871
|
users: 'Usuarios',
|
|
857
872
|
comments: 'Comentarios',
|
|
858
873
|
likes: 'Gustos',
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
l10nCtx.multiPub(ES);
|
|
874
|
+
});
|
|
862
875
|
```
|
|
863
876
|
|
|
864
|
-
|
|
877
|
+
You can also read and modify named context directly from any component using the `$` proxy:
|
|
865
878
|
```js
|
|
866
879
|
class MyComponent extends Symbiote {
|
|
867
880
|
renderCallback() {
|
|
868
|
-
// Read:
|
|
869
881
|
console.log(this.$['L10N/users']);
|
|
870
|
-
|
|
871
|
-
// Modify:
|
|
872
882
|
this.$['L10N/users'] = 'ユーザー';
|
|
873
883
|
}
|
|
874
884
|
}
|
|
875
885
|
```
|
|
876
886
|
|
|
877
|
-
More information about `PubSub` in the [PubSub
|
|
887
|
+
More information about `PubSub` in the [PubSub →](./pubsub.md) section.
|
|
888
|
+
|
|
889
|
+
---
|
|
878
890
|
|
|
879
891
|
## Pop-up context
|
|
880
892
|
|
|
881
|
-
Pop-up
|
|
893
|
+
Pop-up context lets a child component reach a property defined by an ancestor, without the ancestor needing to pass it down explicitly. Symbiote walks up the DOM tree until it finds a component that has the requested property in its data context.
|
|
894
|
+
|
|
895
|
+
Use the `^` token to reference a pop-up property - in both text bindings and event handlers:
|
|
896
|
+
```html
|
|
897
|
+
<!-- Text binding to ancestor property: -->
|
|
898
|
+
<div>{{^parentTitle}}</div>
|
|
899
|
+
|
|
900
|
+
<!-- Handler binding to ancestor method: -->
|
|
901
|
+
<button ${{onclick: '^onButtonClicked'}}>Click me!</button>
|
|
902
|
+
```
|
|
882
903
|
|
|
883
|
-
Use the `^` token to reference a higher-level property:
|
|
884
904
|
```js
|
|
885
|
-
class
|
|
905
|
+
class MyButton extends Symbiote {}
|
|
886
906
|
|
|
887
|
-
|
|
907
|
+
MyButton.template = html`
|
|
908
|
+
<span>{{^parentTitle}}</span>
|
|
888
909
|
<button ${{onclick: '^onButtonClicked'}}>Click me!</button>
|
|
889
910
|
`;
|
|
890
911
|
```
|
|
891
|
-
|
|
912
|
+
|
|
913
|
+
Symbiote walks up the DOM tree until it finds a component with the requested property registered in its context:
|
|
914
|
+
```js
|
|
915
|
+
class MyEditor extends Symbiote {
|
|
916
|
+
init$ = {
|
|
917
|
+
onButtonClicked: () => console.log('clicked'),
|
|
918
|
+
editorTitle: 'My Editor',
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
```
|
|
892
922
|
|
|
893
923
|
> [!IMPORTANT]
|
|
894
|
-
>
|
|
924
|
+
> Pop-up lookup only searches the **data context** (properties registered via `init$` or `add$()`). Plain class properties are not resolved this way. Always declare `^`-targeted properties in the parent's `init$`:
|
|
895
925
|
> ```js
|
|
896
926
|
> class ParentComponent extends Symbiote {
|
|
897
927
|
> init$ = {
|
|
898
|
-
> onButtonClicked: () =>
|
|
928
|
+
> onButtonClicked: () => console.log('clicked'),
|
|
899
929
|
> parentTitle: 'Hello',
|
|
900
930
|
> }
|
|
901
931
|
> }
|
|
902
932
|
> ```
|
|
903
933
|
|
|
904
|
-
|
|
934
|
+
Pop-up context is useful for composition - the same child component adapts to whichever ancestor provides the expected behavior:
|
|
905
935
|
```js
|
|
906
936
|
html`
|
|
907
937
|
<my-text-editor>
|
|
908
938
|
<complete-toolbar></complete-toolbar>
|
|
909
939
|
</my-text-editor>
|
|
910
940
|
`;
|
|
911
|
-
|
|
912
|
-
Or:
|
|
913
|
-
```js
|
|
941
|
+
// or:
|
|
914
942
|
html`
|
|
915
943
|
<my-text-editor>
|
|
916
944
|
<simplified-toolbar></simplified-toolbar>
|
|
@@ -919,19 +947,21 @@ html`
|
|
|
919
947
|
`;
|
|
920
948
|
```
|
|
921
949
|
|
|
922
|
-
> Like
|
|
950
|
+
> Like the CSS cascade, pop-up context has no collision guard. Use additional prefixes (e.g. `myApp_onSave`) in environments you don't fully control.
|
|
951
|
+
|
|
952
|
+
---
|
|
923
953
|
|
|
924
954
|
## Shared context
|
|
925
955
|
|
|
926
|
-
Shared context
|
|
956
|
+
Shared context is inspired by native HTML `name` attributes - the same way `<input name="group">` connects radio buttons into one workflow, the `ctx` attribute connects Symbiote components into a shared data context. Components with the same `ctx` name access a common reactive store with no intermediary component required.
|
|
927
957
|
|
|
928
|
-
|
|
958
|
+
**Assign a context name via the `ctx` HTML attribute:**
|
|
929
959
|
```html
|
|
930
960
|
<upload-btn ctx="gallery"></upload-btn>
|
|
931
961
|
<file-list ctx="gallery"></file-list>
|
|
932
962
|
```
|
|
933
963
|
|
|
934
|
-
Or
|
|
964
|
+
**Or via the `--ctx` CSS custom property**, which cascades like any CSS variable:
|
|
935
965
|
```css
|
|
936
966
|
.gallery-section {
|
|
937
967
|
--ctx: gallery;
|
|
@@ -945,49 +975,64 @@ Or using the `--ctx` CSS custom property:
|
|
|
945
975
|
```
|
|
946
976
|
|
|
947
977
|
The CSS approach is useful when:
|
|
948
|
-
- You want **layout-driven grouping**
|
|
949
|
-
- You need to **
|
|
950
|
-
- You work in a **framework-agnostic setup**
|
|
978
|
+
- You want **layout-driven grouping** - components inherit context from their visual container rather than repeating the attribute on each one
|
|
979
|
+
- You need to **override context at different DOM levels** - just like any CSS custom property, `--ctx` cascades and can be reassigned in nested selectors
|
|
980
|
+
- You work in a **framework-agnostic setup** - CSS context assignment is independent of the host template engine
|
|
951
981
|
|
|
952
|
-
|
|
982
|
+
**Define shared properties with the `*` token:**
|
|
953
983
|
```js
|
|
954
984
|
class UploadBtn extends Symbiote {
|
|
955
985
|
init$ = { '*files': [] }
|
|
956
986
|
|
|
957
|
-
onUpload() {
|
|
987
|
+
onUpload(newFile) {
|
|
958
988
|
this.$['*files'] = [...this.$['*files'], newFile];
|
|
959
989
|
}
|
|
960
990
|
}
|
|
961
991
|
|
|
962
992
|
class FileList extends Symbiote {
|
|
963
|
-
init$ = { '*files': [] } // same shared prop
|
|
993
|
+
init$ = { '*files': [] } // same shared prop - first-registered value wins
|
|
964
994
|
}
|
|
965
995
|
```
|
|
966
996
|
|
|
967
|
-
Both components
|
|
997
|
+
Both components read and write the same `*files` store. When one updates it, the other reacts automatically - no parent component, no prop drilling, no global store.
|
|
968
998
|
|
|
969
999
|
### Context name resolution
|
|
970
1000
|
|
|
971
|
-
The
|
|
1001
|
+
The `ctx` name is resolved in this order (first match wins):
|
|
1002
|
+
|
|
1003
|
+
1. `ctx="name"` HTML attribute on the element
|
|
1004
|
+
2. `--ctx` CSS custom property inherited from ancestors
|
|
972
1005
|
|
|
973
|
-
|
|
974
|
-
2. `--ctx` CSS custom property (inherited from ancestors)
|
|
1006
|
+
> **IMPORTANT**: In Symbiote 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx` variable. Without one, no shared context is created, `*` props have no effect, and dev mode will warn (W6).
|
|
975
1007
|
|
|
976
|
-
|
|
1008
|
+
---
|
|
977
1009
|
|
|
978
1010
|
## CSS Data context
|
|
979
1011
|
|
|
980
|
-
Symbiote components can
|
|
981
|
-
|
|
982
|
-
:
|
|
983
|
-
|
|
984
|
-
|
|
1012
|
+
Symbiote components can initialize their properties from CSS custom property values, enabling CSS-driven configuration: theme tokens, layout parameters, or localized strings - all settable from a stylesheet without touching JavaScript.
|
|
1013
|
+
|
|
1014
|
+
Use `cssInit$` to explicitly declare CSS-initialized properties with fallback values:
|
|
1015
|
+
```js
|
|
1016
|
+
class MyWidget extends Symbiote {
|
|
1017
|
+
cssInit$ = {
|
|
1018
|
+
'--columns': 1,
|
|
1019
|
+
'--label': '',
|
|
1020
|
+
}
|
|
985
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
MyWidget.template = html`
|
|
1024
|
+
<span>{{--label}}</span>
|
|
1025
|
+
`;
|
|
986
1026
|
```
|
|
987
1027
|
|
|
988
|
-
|
|
1028
|
+
```css
|
|
1029
|
+
my-widget {
|
|
1030
|
+
--columns: 3;
|
|
1031
|
+
--label: 'Click me';
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
989
1034
|
|
|
990
|
-
|
|
1035
|
+
You can also use `--` bindings directly in templates without `cssInit$`:
|
|
991
1036
|
```js
|
|
992
1037
|
class TestApp extends Symbiote {}
|
|
993
1038
|
|
|
@@ -996,10 +1041,79 @@ TestApp.template = html`
|
|
|
996
1041
|
<div>{{--text}}</div>
|
|
997
1042
|
`;
|
|
998
1043
|
```
|
|
1044
|
+
```css
|
|
1045
|
+
:root {
|
|
1046
|
+
--header: 'CSS Data';
|
|
1047
|
+
--text: 'Hello!';
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
999
1050
|
|
|
1000
|
-
> CSS custom
|
|
1051
|
+
> CSS custom property values must be valid JSON - use quoted strings (`'text'`), numbers, and `0`/`1` for booleans.
|
|
1052
|
+
|
|
1053
|
+
> CSS properties are used for **initialization only**. After the component mounts, they act as normal local context properties and no longer track CSS changes. Call `this.updateCssData()` to re-read them after runtime CSS updates.
|
|
1054
|
+
|
|
1055
|
+
Full details - `updateCssData()`, `dropCssDataCache()`, `ResizeObserver` patterns, and SSR caveats - in the [CSS Data →](./css-data.md) section.
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
## Choosing the right context type
|
|
1060
|
+
|
|
1061
|
+
| Situation | Use |
|
|
1062
|
+
|-----------|-----|
|
|
1063
|
+
| Component-local reactive state | Local - no token |
|
|
1064
|
+
| Behavior or data passed from an ancestor | Pop-up - `^` |
|
|
1065
|
+
| Sibling components sharing a workflow state | Shared - `*` |
|
|
1066
|
+
| App-wide data, accessed from anywhere in the tree | Named - `/` |
|
|
1067
|
+
| Component configured via CSS / design tokens | CSS Data - `--` |
|
|
1068
|
+
| Reacting to HTML attribute values | Attribute - `@` |
|
|
1069
|
+
| Values derived from other properties | Computed - `+` |
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1001
1072
|
|
|
1002
|
-
|
|
1073
|
+
## All context types in one component
|
|
1074
|
+
|
|
1075
|
+
A concise example showing local, attribute, named, shared, and pop-up contexts working together:
|
|
1076
|
+
|
|
1077
|
+
```js
|
|
1078
|
+
import Symbiote, { html } from '@symbiotejs/symbiote';
|
|
1079
|
+
|
|
1080
|
+
class MyApp extends Symbiote {
|
|
1081
|
+
init$ = {
|
|
1082
|
+
localCtxProp: 'LOCAL',
|
|
1083
|
+
'@attr-test': '', // bound to HTML attribute
|
|
1084
|
+
'APP/namedProp': 'NAMED', // named context
|
|
1085
|
+
'*sharedProp': 'SHARED', // shared context (requires ctx="..." in HTML)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
onUpdate() {
|
|
1089
|
+
let suffix = ' updated';
|
|
1090
|
+
this.$.localCtxProp += suffix;
|
|
1091
|
+
this.$['APP/namedProp'] += suffix;
|
|
1092
|
+
this.$['*sharedProp'] += suffix;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
MyApp.template = html`
|
|
1097
|
+
<div>local: {{localCtxProp}}</div>
|
|
1098
|
+
<div>attribute: {{@attr-test}}</div>
|
|
1099
|
+
<div>named: {{APP/namedProp}}</div>
|
|
1100
|
+
<div>shared: {{*sharedProp}}</div>
|
|
1101
|
+
<button ${{onclick: 'onUpdate'}}>Update</button>
|
|
1102
|
+
<inner-el></inner-el> <!-- reads ^localCtxProp via pop-up -->
|
|
1103
|
+
`;
|
|
1104
|
+
|
|
1105
|
+
MyApp.reg('my-app');
|
|
1106
|
+
|
|
1107
|
+
class InnerEl extends Symbiote {}
|
|
1108
|
+
InnerEl.template = html`<h2>pop-up: {{^localCtxProp}}</h2>`;
|
|
1109
|
+
InnerEl.reg('inner-el');
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
```html
|
|
1113
|
+
<my-app attr-test="HTML value" ctx="my-ctx"></my-app>
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1003
1117
|
|
|
1004
1118
|
## Property token summary
|
|
1005
1119
|
|
|
@@ -1010,8 +1124,8 @@ More details in the [CSS Data](./css-data.md) section.
|
|
|
1010
1124
|
| `*` | Shared | `*sharedProp` |
|
|
1011
1125
|
| `/` | Named | `APP/myProp` |
|
|
1012
1126
|
| `--` | CSS Data | `--my-css-var` |
|
|
1013
|
-
| `@` | Attribute | `@my-attribute` |
|
|
1014
|
-
| `+` | Computed | `+computedProp` |
|
|
1127
|
+
| `@` | Attribute - see [Attributes →](./attributes.md) | `@my-attribute` |
|
|
1128
|
+
| `+` | Computed - see [Properties →](./properties.md#computed-properties) | `+computedProp` |
|
|
1015
1129
|
|
|
1016
1130
|
---
|
|
1017
1131
|
|