@symbiotejs/symbiote 3.8.0 → 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 +25 -120
- package/core/webmcp.js +60 -39
- 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/docs/webmcp.md +5 -9
- package/llms-full.txt +308 -204
- package/llms.txt +0 -1
- package/package.json +1 -2
- package/scripts/build-llms.js +0 -2
- package/types/core/webmcp.d.ts +3 -3
- package/types/core/webmcp.d.ts.map +1 -1
- 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
|
@@ -2,23 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
<img src="https://rnd-pro.com/svg/symbiote/index.svg" width="200" alt="Symbiote.js">
|
|
4
4
|
|
|
5
|
-
A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional.
|
|
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
|
-
|
|
7
|
+
Here are the three most important differences between Symbiote.js and other frameworks:
|
|
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
|
|
8
11
|
|
|
9
|
-
## What's new?
|
|
12
|
+
## What's new in v3.x?
|
|
10
13
|
|
|
11
|
-
- **
|
|
14
|
+
- **WebMCP support** - expose live Symbiote UI actions as browser-native tools for agents. See the [WebMCP docs](https://github.com/symbiotejs/symbiote.js/blob/webmcp/docs/webmcp.md).
|
|
12
15
|
- **Server-Side Rendering** - render components to HTML with `SSR.processHtml()` or stream chunks with `SSR.renderToStream()`. Client-side hydration via `ssrMode` attaches bindings to existing DOM without re-rendering.
|
|
13
16
|
- **Isomorphic components** - `isoMode` flag makes components work in both SSR and client-only scenarios automatically. If server-rendered content exists, it hydrates; otherwise it renders the template from scratch. One component, zero conditional logic.
|
|
14
|
-
- **Computed properties** - reactive derived state with microtask batching.
|
|
17
|
+
- **Computed properties refined** - reactive derived state with microtask batching.
|
|
15
18
|
- **Path-based router** - optional `AppRouter` module with `:param` extraction, route guards, and lazy loading.
|
|
16
19
|
- **Exit animations** - `animateOut(el)` for CSS-driven exit transitions, integrated into itemize API.
|
|
17
20
|
- **Dev mode** - `Symbiote.devMode` enables verbose warnings; import `devMessages.js` for full human-readable messages.
|
|
18
21
|
- **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM.
|
|
19
22
|
- **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods.
|
|
20
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.
|
|
21
|
-
- And [more](https://github.com/symbiotejs/symbiote.js/blob/webmcp/CHANGELOG.md).
|
|
22
24
|
|
|
23
25
|
## Quick start
|
|
24
26
|
|
|
@@ -56,57 +58,6 @@ npm i @symbiotejs/symbiote
|
|
|
56
58
|
import Symbiote, { html, css } from '@symbiotejs/symbiote';
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
## Isomorphic Web Components
|
|
60
|
-
|
|
61
|
-
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:
|
|
62
|
-
```js
|
|
63
|
-
class MyComponent extends Symbiote {
|
|
64
|
-
isoMode = true;
|
|
65
|
-
count = 0;
|
|
66
|
-
increment() {
|
|
67
|
-
this.$.count++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
MyComponent.template = html`
|
|
72
|
-
<h2 ${{textContent: 'count'}}></h2>
|
|
73
|
-
<button ${{onclick: 'increment'}}>Click me!</button>
|
|
74
|
-
`;
|
|
75
|
-
MyComponent.reg('my-component');
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
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.
|
|
79
|
-
|
|
80
|
-
### SSR - one class, zero config
|
|
81
|
-
|
|
82
|
-
Server rendering doesn't need a virtual DOM, a reconciler, or framework-specific packages:
|
|
83
|
-
|
|
84
|
-
```js
|
|
85
|
-
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
86
|
-
|
|
87
|
-
await SSR.init(); // patches globals with linkedom
|
|
88
|
-
await import('./my-app.js'); // components register normally
|
|
89
|
-
|
|
90
|
-
let html = await SSR.processHtml('<my-app></my-app>');
|
|
91
|
-
SSR.destroy();
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
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).
|
|
95
|
-
|
|
96
|
-
### How it compares
|
|
97
|
-
|
|
98
|
-
| | **Symbiote.js** | **Next.js (React)** | **Lit** (`@lit-labs/ssr`) |
|
|
99
|
-
|--|----------------|---------------------|----|
|
|
100
|
-
| **Isomorphic code** | Same code, `isoMode` auto-detects | Server Components vs Client Components split | Same code, but load-order constraints |
|
|
101
|
-
| **Hydration** | Binding-based - attaches to existing DOM, no diffing | `hydrateRoot()` - must produce identical output or errors | Requires `ssr-client` + hydrate support module |
|
|
102
|
-
| **Packages** | 1 module + `linkedom` peer dep | Full framework buy-in | 3 packages: `ssr`, `ssr-client`, `ssr-dom-shim` |
|
|
103
|
-
| **Streaming** | `renderToStream()` async generator | `renderToPipeableStream()` | Iterable `RenderResult` |
|
|
104
|
-
| **Mismatch handling** | Not needed - bindings attach to whatever DOM exists | Hard errors if server/client output differs | N/A |
|
|
105
|
-
| **Template output** | Clean HTML with `bind=` attributes | HTML with framework markers | HTML with `<!--lit-part-->` comment markers |
|
|
106
|
-
| **Lock-in** | None - standard Web Components | Full framework commitment | Lit-specific, but Web Components |
|
|
107
|
-
|
|
108
|
-
**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.
|
|
109
|
-
|
|
110
61
|
## Core concepts
|
|
111
62
|
|
|
112
63
|
### Reactive state
|
|
@@ -135,7 +86,7 @@ This makes it easy to control Symbiote-based widgets and microfrontends from any
|
|
|
135
86
|
|
|
136
87
|
### Templates
|
|
137
88
|
|
|
138
|
-
Templates are plain HTML strings -
|
|
89
|
+
Templates are plain HTML strings - runtime-agnostic, easy to test, easy to move between files:
|
|
139
90
|
|
|
140
91
|
```js
|
|
141
92
|
// Separate file: my-component.template.js
|
|
@@ -153,34 +104,24 @@ The `html` function supports two interpolation modes:
|
|
|
153
104
|
|
|
154
105
|
### Itemize (dynamic reactive lists)
|
|
155
106
|
|
|
156
|
-
Render lists from data arrays with efficient updates:
|
|
107
|
+
Render lists from data arrays or objects with efficient updates:
|
|
157
108
|
```js
|
|
158
109
|
class TaskList extends Symbiote {
|
|
159
110
|
tasks = [
|
|
160
111
|
{ name: 'Buy groceries' },
|
|
161
112
|
{ name: 'Write docs' },
|
|
162
113
|
];
|
|
163
|
-
init$ = {
|
|
164
|
-
// Needs to be defined in init$ for pop-up binding to work
|
|
165
|
-
onItemClick: () => {
|
|
166
|
-
console.log('clicked!');
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
114
|
}
|
|
170
115
|
|
|
171
116
|
TaskList.template = html`
|
|
172
|
-
<
|
|
117
|
+
<ul itemize="tasks">
|
|
173
118
|
<template>
|
|
174
|
-
<
|
|
119
|
+
<li>{{name}}</li>
|
|
175
120
|
</template>
|
|
176
|
-
</
|
|
121
|
+
</ul>
|
|
177
122
|
`;
|
|
178
123
|
```
|
|
179
124
|
|
|
180
|
-
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$`.
|
|
181
|
-
|
|
182
|
-
> **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.
|
|
183
|
-
|
|
184
125
|
### Pop-up binding (`^`)
|
|
185
126
|
|
|
186
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$()`):
|
|
@@ -193,14 +134,11 @@ The `^` prefix works in any nested component template - it walks up the DOM tree
|
|
|
193
134
|
<button ${{onclick: '^parentHandler'}}>Click</button>
|
|
194
135
|
```
|
|
195
136
|
|
|
196
|
-
> **Note:** Class property fallbacks are not checked by the `^` walk - the parent must define the property in `init$`.
|
|
197
|
-
|
|
198
137
|
### Named data contexts
|
|
199
138
|
|
|
200
139
|
Share state across components without prop drilling:
|
|
201
|
-
|
|
202
140
|
```js
|
|
203
|
-
import { PubSub } from '@symbiotejs/symbiote';
|
|
141
|
+
import { PubSub, html } from '@symbiotejs/symbiote';
|
|
204
142
|
|
|
205
143
|
PubSub.registerCtx({
|
|
206
144
|
user: 'Alex',
|
|
@@ -209,6 +147,9 @@ PubSub.registerCtx({
|
|
|
209
147
|
|
|
210
148
|
// Any component can read/write:
|
|
211
149
|
this.$['APP/user'] = 'New name';
|
|
150
|
+
|
|
151
|
+
// Any template can use property directly:
|
|
152
|
+
let template = html`<h2>{{APP/user}}</h2>`;
|
|
212
153
|
```
|
|
213
154
|
|
|
214
155
|
### Shared context (`*`)
|
|
@@ -241,11 +182,10 @@ class StatusBar extends Symbiote {
|
|
|
241
182
|
|
|
242
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.
|
|
243
184
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
### Routing (optional module)
|
|
185
|
+
### Application routing
|
|
247
186
|
|
|
248
187
|
```js
|
|
188
|
+
// Import optional module:
|
|
249
189
|
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
250
190
|
|
|
251
191
|
AppRouter.initRoutingCtx('R', {
|
|
@@ -255,23 +195,7 @@ AppRouter.initRoutingCtx('R', {
|
|
|
255
195
|
});
|
|
256
196
|
```
|
|
257
197
|
|
|
258
|
-
###
|
|
259
|
-
|
|
260
|
-
CSS-driven transitions with zero JS animation code:
|
|
261
|
-
|
|
262
|
-
```css
|
|
263
|
-
task-item {
|
|
264
|
-
opacity: 1;
|
|
265
|
-
transition: opacity 0.3s;
|
|
266
|
-
|
|
267
|
-
@starting-style { opacity: 0; } /* enter */
|
|
268
|
-
&[leaving] { opacity: 0; } /* exit */
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
`animateOut(el)` sets `[leaving]`, waits for `transitionend`, then removes. Itemize uses this automatically.
|
|
273
|
-
|
|
274
|
-
### Styling
|
|
198
|
+
### CSS Styling
|
|
275
199
|
|
|
276
200
|
Shadow DOM is **optional** in Symbiote - use it when you need isolation, skip it when you don't. This gives full flexibility:
|
|
277
201
|
|
|
@@ -288,6 +212,8 @@ MyComponent.rootStyles = css`
|
|
|
288
212
|
`;
|
|
289
213
|
```
|
|
290
214
|
|
|
215
|
+
This style will be applied to nearest upper shadow root, if exists and to common document if not.
|
|
216
|
+
|
|
291
217
|
**Shadow DOM** - opt-in isolation when needed:
|
|
292
218
|
|
|
293
219
|
```js
|
|
@@ -301,9 +227,9 @@ Isolated.shadowStyles = css`
|
|
|
301
227
|
|
|
302
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.
|
|
303
229
|
|
|
304
|
-
### CSS Data
|
|
230
|
+
### CSS Data
|
|
305
231
|
|
|
306
|
-
Components can read CSS custom
|
|
232
|
+
Components can read CSS custom property values to initiate reactive state:
|
|
307
233
|
|
|
308
234
|
```css
|
|
309
235
|
my-widget {
|
|
@@ -319,8 +245,6 @@ MyWidget.template = html`
|
|
|
319
245
|
`;
|
|
320
246
|
```
|
|
321
247
|
|
|
322
|
-
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.
|
|
323
|
-
|
|
324
248
|
## Best for
|
|
325
249
|
|
|
326
250
|
- **Complex widgets** embedded in any host application
|
|
@@ -329,32 +253,15 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
|
|
|
329
253
|
- **Reusable component libraries** - works in React, Vue, Angular, or plain HTML
|
|
330
254
|
- **SSR-powered apps** - lightweight server rendering without framework lock-in
|
|
331
255
|
- **Framework-agnostic solutions** - one codebase, any context
|
|
332
|
-
|
|
333
|
-
## Bundle size
|
|
334
|
-
|
|
335
|
-
| Library | Minified | Gzip | Brotli |
|
|
336
|
-
|---------|----------|------|--------|
|
|
337
|
-
| **Symbiote.js** (core) | 23.6 kb | 8.1 kb | **7.3 kb** |
|
|
338
|
-
| **Symbiote.js** (full, with AppRouter + WebMCP export) | 35.6 kb | 12.1 kb | **11.0 kb** |
|
|
339
|
-
| **Symbiote.js** (WebMCP extension) | 31.4 kb | 10.6 kb | **9.6 kb** |
|
|
340
|
-
| **Lit** 3.3 | 15.5 kb | 6.0 kb | **~5.1 kb** |
|
|
341
|
-
| **React 19 + ReactDOM** | ~186 kb | ~59 kb | **~50 kb** |
|
|
342
|
-
|
|
343
|
-
Symbiote and Lit have similar base sizes, but Symbiote's **7.3 kb** core includes more built-in features: global state management, lists (itemize API), exit animations, computed properties etc. Lit needs additional packages for comparable features. React is **~7× larger** before adding a router, state manager, or SSR framework.
|
|
344
|
-
|
|
345
|
-
## Browser support
|
|
346
|
-
|
|
347
|
-
All modern browsers: Chrome, Firefox, Safari, Edge, Opera.
|
|
256
|
+
- **Modern AI-first web** - expose the application state to WebMCP tools automatically
|
|
348
257
|
|
|
349
258
|
## Docs & Examples
|
|
350
259
|
|
|
351
260
|
- [Documentation](https://github.com/symbiotejs/symbiote.js/blob/main/docs/README.md)
|
|
352
|
-
- [Lit vs Symbiote.js](https://github.com/symbiotejs/symbiote.js/blob/main/docs/lit-vs-symbiote.md) - Side-by-side comparison
|
|
353
261
|
- [Live Examples](https://rnd-pro.com/symbiote/3x/examples/) - Interactive Code Playground
|
|
354
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
|
|
355
263
|
- [AI / llms.txt](https://rnd-pro.com/symbiote/llms.txt) — index for AI tools
|
|
356
264
|
- [Full docs (single file)](https://rnd-pro.com/symbiote/llms-full.txt) — complete merged reference for AI context
|
|
357
|
-
- [Changelog](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md)
|
|
358
265
|
|
|
359
266
|
## Related articles
|
|
360
267
|
|
|
@@ -411,7 +318,7 @@ my-item {
|
|
|
411
318
|
|
|
412
319
|
## Itemize integration
|
|
413
320
|
|
|
414
|
-
|
|
321
|
+
Itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
|
|
415
322
|
```css
|
|
416
323
|
user-card {
|
|
417
324
|
opacity: 1;
|
|
@@ -455,7 +362,7 @@ class MyComponent extends Symbiote {}
|
|
|
455
362
|
MyComponent.template = html`<h1>{{@attribute-name}}</h1>`;
|
|
456
363
|
```
|
|
457
364
|
|
|
458
|
-
Then use it as an attribute in markup:
|
|
365
|
+
Then use it as an component's attribute in markup:
|
|
459
366
|
```html
|
|
460
367
|
<my-component attribute-name="attribute value"></my-component>
|
|
461
368
|
```
|
|
@@ -479,7 +386,7 @@ MyComponent.observedAttributes = [
|
|
|
479
386
|
|
|
480
387
|
## `bindAttributes()` static method
|
|
481
388
|
|
|
482
|
-
|
|
389
|
+
Reflect attribute values to property values:
|
|
483
390
|
```js
|
|
484
391
|
class MyComponent extends Symbiote {
|
|
485
392
|
|
|
@@ -760,7 +667,69 @@ Named external contexts (`CTX/prop`) are different: they are registered globally
|
|
|
760
667
|
|
|
761
668
|
---
|
|
762
669
|
|
|
763
|
-
## 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
|
|
764
733
|
|
|
765
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.
|
|
766
735
|
|
|
@@ -786,39 +755,46 @@ class MyComponent extends Symbiote {
|
|
|
786
755
|
|
|
787
756
|
# Context
|
|
788
757
|
|
|
789
|
-
|
|
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.
|
|
790
759
|
|
|
791
|
-
|
|
760
|
+
There are seven context types, each addressed by a token prefix in property keys:
|
|
761
|
+
|
|
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
|
+
---
|
|
792
773
|
|
|
793
774
|
## Local context
|
|
794
775
|
|
|
795
|
-
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$`:
|
|
796
779
|
```js
|
|
797
780
|
class MyComponent extends Symbiote {
|
|
798
|
-
|
|
799
781
|
init$ = {
|
|
800
782
|
myProperty: 'some value',
|
|
801
783
|
}
|
|
802
|
-
|
|
803
784
|
}
|
|
804
785
|
```
|
|
805
786
|
|
|
806
|
-
Use the `$` proxy to read and write values:
|
|
787
|
+
Use the `$` proxy to read and write values at runtime:
|
|
807
788
|
```js
|
|
808
789
|
class MyComponent extends Symbiote {
|
|
809
|
-
|
|
810
790
|
init$ = {
|
|
811
791
|
myProperty: 'some value',
|
|
812
792
|
}
|
|
813
793
|
|
|
814
794
|
renderCallback() {
|
|
815
|
-
// Read:
|
|
816
795
|
console.log(this.$.myProperty); // > 'some value'
|
|
817
|
-
|
|
818
|
-
// Write:
|
|
819
796
|
this.$.myProperty = 'new value';
|
|
820
797
|
}
|
|
821
|
-
|
|
822
798
|
}
|
|
823
799
|
|
|
824
800
|
MyComponent.template = html`
|
|
@@ -826,97 +802,143 @@ MyComponent.template = html`
|
|
|
826
802
|
`;
|
|
827
803
|
```
|
|
828
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
|
+
|
|
829
819
|
## Named context
|
|
830
820
|
|
|
831
|
-
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.
|
|
822
|
+
|
|
823
|
+
Use named context when data must be shared across unrelated parts of the application.
|
|
832
824
|
|
|
833
|
-
|
|
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:**
|
|
834
854
|
```js
|
|
835
855
|
import Symbiote, { html, PubSub } from '@symbiotejs/symbiote';
|
|
836
856
|
|
|
837
|
-
|
|
838
|
-
let EN = {
|
|
857
|
+
let l10nCtx = PubSub.registerCtx({
|
|
839
858
|
users: 'Users',
|
|
840
859
|
comments: 'Comments',
|
|
841
860
|
likes: 'Likes',
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
// Create localization context:
|
|
845
|
-
let l10nCtx = PubSub.registerCtx(EN, 'L10N');
|
|
846
|
-
|
|
847
|
-
// Use localized strings in templates:
|
|
848
|
-
class MyComponent extends Symbiote { ... }
|
|
861
|
+
}, 'L10N');
|
|
849
862
|
|
|
850
863
|
MyComponent.template = html`
|
|
851
864
|
<div>{{L10N/users}} - {{numberOfUsers}}</div>
|
|
852
865
|
<div>{{L10N/comments}} - {{numberOfComments}}</div>
|
|
853
866
|
<div>{{L10N/likes}} - {{numberOfLikes}}</div>
|
|
854
867
|
`;
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
Use `/` token with the context key to access properties: `L10N/users`.
|
|
858
868
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
let ES = {
|
|
869
|
+
// Switch language at any time - all subscribed components update instantly:
|
|
870
|
+
l10nCtx.multiPub({
|
|
862
871
|
users: 'Usuarios',
|
|
863
872
|
comments: 'Comentarios',
|
|
864
873
|
likes: 'Gustos',
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
l10nCtx.multiPub(ES);
|
|
874
|
+
});
|
|
868
875
|
```
|
|
869
876
|
|
|
870
|
-
|
|
877
|
+
You can also read and modify named context directly from any component using the `$` proxy:
|
|
871
878
|
```js
|
|
872
879
|
class MyComponent extends Symbiote {
|
|
873
880
|
renderCallback() {
|
|
874
|
-
// Read:
|
|
875
881
|
console.log(this.$['L10N/users']);
|
|
876
|
-
|
|
877
|
-
// Modify:
|
|
878
882
|
this.$['L10N/users'] = 'ユーザー';
|
|
879
883
|
}
|
|
880
884
|
}
|
|
881
885
|
```
|
|
882
886
|
|
|
883
|
-
More information about `PubSub` in the [PubSub
|
|
887
|
+
More information about `PubSub` in the [PubSub →](./pubsub.md) section.
|
|
888
|
+
|
|
889
|
+
---
|
|
884
890
|
|
|
885
891
|
## Pop-up context
|
|
886
892
|
|
|
887
|
-
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
|
+
```
|
|
888
903
|
|
|
889
|
-
Use the `^` token to reference a higher-level property:
|
|
890
904
|
```js
|
|
891
|
-
class
|
|
905
|
+
class MyButton extends Symbiote {}
|
|
892
906
|
|
|
893
|
-
|
|
907
|
+
MyButton.template = html`
|
|
908
|
+
<span>{{^parentTitle}}</span>
|
|
894
909
|
<button ${{onclick: '^onButtonClicked'}}>Click me!</button>
|
|
895
910
|
`;
|
|
896
911
|
```
|
|
897
|
-
|
|
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
|
+
```
|
|
898
922
|
|
|
899
923
|
> [!IMPORTANT]
|
|
900
|
-
>
|
|
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$`:
|
|
901
925
|
> ```js
|
|
902
926
|
> class ParentComponent extends Symbiote {
|
|
903
927
|
> init$ = {
|
|
904
|
-
> onButtonClicked: () =>
|
|
928
|
+
> onButtonClicked: () => console.log('clicked'),
|
|
905
929
|
> parentTitle: 'Hello',
|
|
906
930
|
> }
|
|
907
931
|
> }
|
|
908
932
|
> ```
|
|
909
933
|
|
|
910
|
-
|
|
934
|
+
Pop-up context is useful for composition - the same child component adapts to whichever ancestor provides the expected behavior:
|
|
911
935
|
```js
|
|
912
936
|
html`
|
|
913
937
|
<my-text-editor>
|
|
914
938
|
<complete-toolbar></complete-toolbar>
|
|
915
939
|
</my-text-editor>
|
|
916
940
|
`;
|
|
917
|
-
|
|
918
|
-
Or:
|
|
919
|
-
```js
|
|
941
|
+
// or:
|
|
920
942
|
html`
|
|
921
943
|
<my-text-editor>
|
|
922
944
|
<simplified-toolbar></simplified-toolbar>
|
|
@@ -925,19 +947,21 @@ html`
|
|
|
925
947
|
`;
|
|
926
948
|
```
|
|
927
949
|
|
|
928
|
-
> 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
|
+
---
|
|
929
953
|
|
|
930
954
|
## Shared context
|
|
931
955
|
|
|
932
|
-
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.
|
|
933
957
|
|
|
934
|
-
|
|
958
|
+
**Assign a context name via the `ctx` HTML attribute:**
|
|
935
959
|
```html
|
|
936
960
|
<upload-btn ctx="gallery"></upload-btn>
|
|
937
961
|
<file-list ctx="gallery"></file-list>
|
|
938
962
|
```
|
|
939
963
|
|
|
940
|
-
Or
|
|
964
|
+
**Or via the `--ctx` CSS custom property**, which cascades like any CSS variable:
|
|
941
965
|
```css
|
|
942
966
|
.gallery-section {
|
|
943
967
|
--ctx: gallery;
|
|
@@ -951,49 +975,64 @@ Or using the `--ctx` CSS custom property:
|
|
|
951
975
|
```
|
|
952
976
|
|
|
953
977
|
The CSS approach is useful when:
|
|
954
|
-
- You want **layout-driven grouping**
|
|
955
|
-
- You need to **
|
|
956
|
-
- 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
|
|
957
981
|
|
|
958
|
-
|
|
982
|
+
**Define shared properties with the `*` token:**
|
|
959
983
|
```js
|
|
960
984
|
class UploadBtn extends Symbiote {
|
|
961
985
|
init$ = { '*files': [] }
|
|
962
986
|
|
|
963
|
-
onUpload() {
|
|
987
|
+
onUpload(newFile) {
|
|
964
988
|
this.$['*files'] = [...this.$['*files'], newFile];
|
|
965
989
|
}
|
|
966
990
|
}
|
|
967
991
|
|
|
968
992
|
class FileList extends Symbiote {
|
|
969
|
-
init$ = { '*files': [] } // same shared prop
|
|
993
|
+
init$ = { '*files': [] } // same shared prop - first-registered value wins
|
|
970
994
|
}
|
|
971
995
|
```
|
|
972
996
|
|
|
973
|
-
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.
|
|
974
998
|
|
|
975
999
|
### Context name resolution
|
|
976
1000
|
|
|
977
|
-
The
|
|
1001
|
+
The `ctx` name is resolved in this order (first match wins):
|
|
978
1002
|
|
|
979
|
-
1. `ctx="name"` HTML attribute
|
|
980
|
-
2. `--ctx` CSS custom property
|
|
1003
|
+
1. `ctx="name"` HTML attribute on the element
|
|
1004
|
+
2. `--ctx` CSS custom property inherited from ancestors
|
|
981
1005
|
|
|
982
|
-
> **IMPORTANT**: In 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx`
|
|
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).
|
|
1007
|
+
|
|
1008
|
+
---
|
|
983
1009
|
|
|
984
1010
|
## CSS Data context
|
|
985
1011
|
|
|
986
|
-
Symbiote components can
|
|
987
|
-
|
|
988
|
-
:
|
|
989
|
-
|
|
990
|
-
|
|
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
|
+
}
|
|
991
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
MyWidget.template = html`
|
|
1024
|
+
<span>{{--label}}</span>
|
|
1025
|
+
`;
|
|
992
1026
|
```
|
|
993
1027
|
|
|
994
|
-
|
|
1028
|
+
```css
|
|
1029
|
+
my-widget {
|
|
1030
|
+
--columns: 3;
|
|
1031
|
+
--label: 'Click me';
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
995
1034
|
|
|
996
|
-
|
|
1035
|
+
You can also use `--` bindings directly in templates without `cssInit$`:
|
|
997
1036
|
```js
|
|
998
1037
|
class TestApp extends Symbiote {}
|
|
999
1038
|
|
|
@@ -1002,10 +1041,79 @@ TestApp.template = html`
|
|
|
1002
1041
|
<div>{{--text}}</div>
|
|
1003
1042
|
`;
|
|
1004
1043
|
```
|
|
1044
|
+
```css
|
|
1045
|
+
:root {
|
|
1046
|
+
--header: 'CSS Data';
|
|
1047
|
+
--text: 'Hello!';
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1005
1050
|
|
|
1006
|
-
> 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
|
+
---
|
|
1072
|
+
|
|
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
|
+
}
|
|
1007
1087
|
|
|
1008
|
-
|
|
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
|
+
---
|
|
1009
1117
|
|
|
1010
1118
|
## Property token summary
|
|
1011
1119
|
|
|
@@ -1016,8 +1124,8 @@ More details in the [CSS Data](./css-data.md) section.
|
|
|
1016
1124
|
| `*` | Shared | `*sharedProp` |
|
|
1017
1125
|
| `/` | Named | `APP/myProp` |
|
|
1018
1126
|
| `--` | CSS Data | `--my-css-var` |
|
|
1019
|
-
| `@` | Attribute | `@my-attribute` |
|
|
1020
|
-
| `+` | Computed | `+computedProp` |
|
|
1127
|
+
| `@` | Attribute - see [Attributes →](./attributes.md) | `@my-attribute` |
|
|
1128
|
+
| `+` | Computed - see [Properties →](./properties.md#computed-properties) | `+computedProp` |
|
|
1021
1129
|
|
|
1022
1130
|
---
|
|
1023
1131
|
|
|
@@ -5149,15 +5257,9 @@ Use Symbiote dev/runtime diagnostics for template binding correctness, because t
|
|
|
5149
5257
|
|
|
5150
5258
|
---
|
|
5151
5259
|
|
|
5152
|
-
# WebMCP
|
|
5153
|
-
|
|
5154
|
-
Symbiote.js can expose the current browser UI state as native WebMCP tools. This feature is experimental and intended for testing with browser builds that expose native WebMCP APIs, such as Chrome Canary 150.
|
|
5260
|
+
# WebMCP
|
|
5155
5261
|
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
```shell
|
|
5159
|
-
npm i @symbiotejs/symbiote@webmcp
|
|
5160
|
-
```
|
|
5262
|
+
Symbiote.js can expose the current browser UI state as native WebMCP tools.
|
|
5161
5263
|
|
|
5162
5264
|
## Activation
|
|
5163
5265
|
|
|
@@ -5165,7 +5267,7 @@ WebMCP is optional. Import the extension before WebMCP-enabled components render
|
|
|
5165
5267
|
|
|
5166
5268
|
```js
|
|
5167
5269
|
import Symbiote, { html } from '@symbiotejs/symbiote';
|
|
5168
|
-
import
|
|
5270
|
+
import '@symbiotejs/symbiote/webmcp';
|
|
5169
5271
|
```
|
|
5170
5272
|
|
|
5171
5273
|
## Automatic Tools
|
|
@@ -5215,6 +5317,8 @@ removeItem_in_task-list_task-item_alpha
|
|
|
5215
5317
|
Use `ToolDescriptor` for descriptions, input schemas, execution logic, and dynamic visibility:
|
|
5216
5318
|
|
|
5217
5319
|
```js
|
|
5320
|
+
import { ToolDescriptor } from '@symbiotejs/symbiote/webmcp';
|
|
5321
|
+
|
|
5218
5322
|
class WebmcpPanel extends Symbiote {
|
|
5219
5323
|
componentDescription = async () => {
|
|
5220
5324
|
return 'Visible order editor with selected item state.';
|