@symbiotejs/symbiote
Version:
Symbiote.js - zero-dependency close-to-platform frontend library to build super-powered web components
836 lines (649 loc) • 27.6 kB
Markdown
# Symbiote.js — AI Context Reference (v3.x)
> **Purpose**: Authoritative reference for AI code assistants. All information is derived from source code analysis of [symbiote.js](https://github.com/symbiotejs/symbiote.js).
> Current version: **3.4.7**. Zero dependencies. ~6.4 KB brotli / ~7.1 KB gzip.
---
## Installation & Import
```js
// NPM
import Symbiote, { html, css, PubSub, DICT } from '@symbiotejs/symbiote';
// CDN / HTTPS
import Symbiote, { html, css } from 'https://esm.run/@symbiotejs/symbiote';
// Individual module imports (tree-shaking)
import Symbiote from '@symbiotejs/symbiote/core/Symbiote.js';
import { PubSub } from '@symbiotejs/symbiote/core/PubSub.js';
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
import { html } from '@symbiotejs/symbiote/core/html.js';
import { css } from '@symbiotejs/symbiote/core/css.js';
```
### Core exports (index.js)
`Symbiote` (default), `html`, `css`, `PubSub`, `DICT`, `animateOut`
### Utils exports (`@symbiotejs/symbiote/utils`)
`UID`, `setNestedProp`, `applyStyles`, `applyAttributes`, `create`, `kebabToCamel`, `reassignDictionary`
---
## Component Basics
Symbiote extends `HTMLElement`. Every component is a Custom Element.
```js
class MyComponent extends Symbiote {
// Class properties as initial values (fallback)
name = 'World';
count = 0;
onBtnClick() {
this.$.count++;
}
}
// Template — assigned via static property SETTER, outside the class body
MyComponent.template = html`
<h1>Hello {{name}}!</h1>
<p>Count: {{count}}</p>
<button ${{onclick: 'onBtnClick'}}>Increment</button>
`;
// Register Custom Element tag
MyComponent.reg('my-component');
```
> **CRITICAL**: `template` is a **static property setter** on the `Symbiote` class, not a regular static class field.
> You **MUST** assign it **outside** the class body: `MyComponent.template = html\`...\``.
> Using `static template = html\`...\`` inside the class declaration **will NOT work**.
> Templates are plain HTML strings, NOT JSX. Use the `html` tagged template literal.
### Lifecycle callbacks (override in subclass)
| Method | When called |
|--------|------------|
| `initCallback()` | Once, after state initialized, before render (if `pauseRender=true`) or normally after render |
| `renderCallback()` | Once, after template is rendered and attached to DOM |
| `destroyCallback()` | On disconnect, after 100ms delay, only if `readyToDestroy=true` |
### Usage in HTML
```html
<my-component></my-component>
```
---
## Template Binding Syntax
Use the `html` tagged template literal for ergonomic binding syntax. It supports **two interpolation modes**:
- **`Object`** → converted to `bind="prop:key;"` attribute (reactive binding)
- **`string` / `number`** → concatenated as-is (native interpolation, useful for SSR page shells)
This dual-mode design means `html` works for both component templates and full-page SSR output — no separate "server-only template" function is needed.
### Text node binding
```html
<div>{{propName}}</div>
```
Binds `propName` from component state to the text content of a text node. Works inside any element. Multiple bindings in one text node are supported: `{{first}} - {{second}}`.
### Property binding (element's own properties)
```html
<button ${{onclick: 'handlerName'}}>Click</button>
<div>{{myProp}}</div>
```
The `${{key: 'value'}}` interpolation creates a `bind="key:value;"` attribute. Keys are DOM element property names. Values are component state property names (strings).
**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:
```js
class MyComp extends Symbiote {
// Approach 1: state property in init$
init$ = { count: 0 };
// Approach 2: class property / method (fallback)
label = 'Click me';
onSubmit() { console.log('submitted'); }
}
```
> **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.
### Nested property binding
```html
<div ${{'style.color': 'colorProp'}}>Text</div>
```
Dot notation navigates nested properties on the element.
### Direct child component state binding
```html
<child-component ${{'$.innerProp': 'parentProp'}}></child-component>
```
The `$.` prefix accesses the child component's `$` state proxy directly.
### Attribute binding (`@` prefix)
```html
<div ${{'@hidden': 'isHidden'}}>Content</div>
<input ${{'@disabled': 'isDisabled'}}>
<div ${{'@data-value': 'myValue'}}></div>
```
The `@` prefix means "bind to HTML attribute" (not DOM property). For boolean attributes: `true` → attribute present, `false` → attribute removed. `@` is for binding syntax only, do NOT use it as a regular HTML attribute prefix.
### Type casting (`!` / `!!`)
```html
<div ${{'@hidden': '!showContent'}}>...</div> <!-- inverted boolean -->
<div ${{'@contenteditable': '!!hasText'}}>...</div> <!-- double inversion = cast to boolean -->
```
### Loose-coupling alternative (plain HTML, no JS context needed)
```html
<div bind="textContent: myProp"></div>
<div bind="onclick: handler; @hidden: !flag"></div>
```
This is the raw form. The `html` helper generates it automatically.
---
## Property Token Prefixes
Prefixes control which data context a binding resolves to:
| Prefix | Meaning | Example | Description |
|--------|---------|---------|-------------|
| _(none)_ | Local state | `{{count}}` | Current component's local context |
| `^` | Pop-up | `{{^parentProp}}` | Walk up DOM ancestry to find nearest component that has this prop in its data context (`init$` / `add$()`) |
| `*` | Shared context | `{{*sharedProp}}` | Shared context scoped by `ctx` attribute or CSS `--ctx` |
| `/` | Named context | `{{APP/myProp}}` | Global named context identified by key before `/` |
| `--` | CSS Data | `{{--my-css-var}}` | Read value from CSS custom property |
| `+` | Computed | (in init$) `'+sum': () => ...` | Function recalculated when local dependencies change (auto-tracked) |
### Examples in init$
```js
init$ = {
localProp: 'hello', // local (prefer class properties for simple cases)
'*sharedProp': 'shared value', // shared context (requires init$)
'APP/globalProp': 42, // named context (requires init$)
'+computed': () => this.$.a + this.$.b, // local computed (requires init$)
};
```
### Computed properties (v3.x)
Computed props use the `+` prefix and are auto-tracked: dependencies are recorded when the function executes.
**Local computed** — reacts to local state changes automatically:
```js
init$ = {
a: 1,
b: 2,
'+sum': () => this.$.a + this.$.b, // auto-tracks 'a' and 'b'
};
```
**Cross-context computed** — reacts to external named context changes via explicit deps:
```js
init$ = {
local: 0,
'+total': {
deps: ['GAME/score'],
fn: () => this.$['GAME/score'] + this.$.local,
},
};
```
> **NOTE**: Computed values are recalculated asynchronously (via `queueMicrotask`), so subscribers are notified in the next microtask, not inline during `pub()`.
```
---
## State Management API
### `$` proxy (read/write state)
```js
this.$.count = 10; // publish
let val = this.$.count; // read
this.$['APP/prop'] = 'x'; // named context
this.$['^parentProp'] = 5; // parent context
```
### `set$(kvObj, forcePrimitives?)` — bulk update
```js
this.set$({ name: 'Jane', count: 5 });
// forcePrimitives=true → triggers callbacks even if value unchanged (for primitives)
```
### `sub(prop, handler, init?)` — subscribe to changes
```js
this.sub('count', (val) => {
console.log('count changed:', val);
});
// init defaults to true (handler called immediately with current value)
```
### `add(prop, val, rewrite?)` — add property to context
### `add$(obj, rewrite?)` — bulk add
### `has(prop)` — check if property exists in context
### `notify(prop)` — force notification to all subscribers
> **WARNING**: Property keys with nested dots (`prop.sub`) are NOT supported as state keys.
> Use flat names: `propSub` instead of `prop.sub`.
---
## PubSub (Standalone State Management)
```js
import { PubSub } from '@symbiotejs/symbiote';
// Register a named global context
const ctx = PubSub.registerCtx({
userName: 'Anonymous',
score: 0,
}, 'GAME'); // 'GAME' is the context key
// Read/write from any component
this.$['GAME/userName'] = 'Player 1';
console.log(this.$['GAME/score']);
// Subscribe from any component
this.sub('GAME/score', (val) => {
console.log('Score:', val);
});
// Direct PubSub API
ctx.pub('score', 100);
ctx.read('score');
ctx.sub('score', callback);
ctx.multiPub({ score: 100, userName: 'Hero' });
```
### PubSub static methods
- `PubSub.registerCtx(schema, uid?)` → `PubSub` instance
- `PubSub.getCtx(uid, notify?)` → `PubSub` instance or null
- `PubSub.deleteCtx(uid)`
---
## Shared Context (`*` prefix)
Components grouped by the `ctx` HTML attribute (or `--ctx` CSS custom property) share a data context. Properties with `*` prefix are read/written in this shared context — inspired by native HTML `name` attribute grouping (like radio button groups):
```html
<upload-btn ctx="gallery"></upload-btn>
<file-list ctx="gallery"></file-list>
```
```js
class UploadBtn extends Symbiote {
init$ = { '*files': [] }
onUpload() {
this.$['*files'] = [...this.$['*files'], newFile];
}
}
class FileList extends Symbiote {
init$ = { '*files': [] } // same shared prop — first-registered value wins
}
```
Both components access the same `*files` state — no parent component, no prop drilling, no global store. Just set `ctx="gallery"` in HTML and use `*`-prefixed properties.
### Context name resolution (first match wins)
1. `ctx="name"` HTML attribute
2. `--ctx` CSS custom property (inherited from ancestors)
3. No match → `*` props are **silently skipped** (dev mode warns)
> **WARNING**: `*` properties require an explicit `ctx` attribute or `--ctx` CSS variable. Without one, the shared context is not created and `*` props have no effect.
---
## Lifecycle & Instance Properties
### Lifecycle callbacks (override in subclass)
| Method | When called |
|--------|------------|
| `initCallback()` | Once, after state initialized, before render (if `pauseRender=true`) or normally after render |
| `renderCallback()` | Once, after template is rendered and attached to DOM |
| `destroyCallback()` | On disconnect, after 100ms delay, only if `readyToDestroy=true` |
### Constructor flags (set in constructor or as class fields)
| Property | Default | Description |
|----------|---------|-------------|
| `pauseRender` | `false` | Skip automatic rendering; call `this.render()` manually later |
| `renderShadow` | `false` | Render template into Shadow DOM |
| `readyToDestroy` | `true` | Allow cleanup on disconnect |
| `processInnerHtml` | `false` | Process existing inner HTML with template processors |
| `ssrMode` | `false` | **Client-only.** Hydrate server-rendered HTML: skips template injection, attaches bindings to existing DOM. Supports both light DOM and Declarative Shadow DOM. Ignored when `__SYMBIOTE_SSR` is active (server side) |
| `isoMode` | `false` | **Client-only.** Isomorphic mode: if component has children at connect time, behaves as `ssrMode = true` (hydrate). If no children, renders template normally. Same component works for both SSR and client-only scenarios |
| `allowCustomTemplate` | `false` | Allow `use-template="#selector"` attribute |
| `isVirtual` | `false` | Replace element with its template fragment |
| `allowTemplateInits` | `true` | Auto-add props found in template but not in init$ |
### Instance properties (available after render)
- `this.ref` — object map of `ref`-attributed elements
- `this.initChildren` — array of original child nodes (before template render)
- `this.$` — state proxy
- `this.allSubs` — Set of all subscriptions (for cleanup)
---
## Exit Animation (`animateOut`)
`animateOut(el)` sets `[leaving]` attribute, waits for CSS `transitionend`, then removes the element. If no CSS transition is defined, removes immediately. Available as standalone import or `Symbiote.animateOut`.
```js
import { animateOut } from '@symbiotejs/symbiote';
// or: Symbiote.animateOut(el)
```
### CSS pattern
```css
my-item {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
/* Enter (CSS-native, no JS needed): */
@starting-style {
opacity: 0;
transform: translateY(20px);
}
/* Exit (triggered by animateOut): */
&[leaving] {
opacity: 0;
transform: translateY(-10px);
}
}
```
### Itemize integration
Both itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM.
---
## Styling
### rootStyles (Light DOM, adopted stylesheets)
```js
MyComponent.rootStyles = css`
my-component {
display: block;
color: var(--text-color);
}
`;
```
Styles are added to the closest document root via `adoptedStyleSheets`. Use the custom tag name as selector.
### shadowStyles (Shadow DOM, auto-creates shadow root)
```js
MyComponent.shadowStyles = css`
:host {
display: block;
}
button {
color: red;
}
`;
```
Setting `shadowStyles` automatically creates a Shadow Root and uses `adoptedStyleSheets`.
### addRootStyles / addShadowStyles (append additional sheets)
```js
MyComponent.addRootStyles(anotherSheet);
MyComponent.addShadowStyles(anotherSheet);
```
### `css` tag function
Returns a `CSSStyleSheet` instance (constructable stylesheet). Supports processors:
```js
css.useProcessor((txt) => txt.replaceAll('$accent', '#ff0'));
```
---
## Element References
```js
MyComponent.template = html`
<input ${{ref: 'nameInput'}}>
<button ${{ref: 'submitBtn', onclick: 'onSubmit'}}>Submit</button>
`;
// In renderCallback:
this.ref.nameInput.focus();
this.ref.submitBtn.disabled = true;
```
Alternative HTML syntax: `<div ref="myRef"></div>`
---
## Itemize API (Dynamic Lists)
```js
class MyList extends Symbiote {
init$ = {
items: [
{ name: 'Alice', role: 'Admin' },
{ name: 'Bob', role: 'User' },
],
}
onItemClick(e) {
console.log('clicked');
}
}
MyList.template = html`
<ul itemize="items">
<template>
<li>
<span>{{name}}</span> - <span>{{role}}</span>
<button ${{onclick: '^onItemClick'}}>Click</button>
</li>
</template>
</ul>
`;
```
> **CRITICAL**: Inside itemize, each item is a Symbiote component with its own state scope.
> There are **two patterns** — they determine how event handlers are bound:
>
> **Pattern 1: Inline `<template>` (dumb items)** — items have no class definition, only data properties from the array.
> Any event handler or data must come from an **external context** — not from the item itself.
> All context prefixes work: `^` (parent pop-up), `APP/` (named), `*` (shared). The most common is `^`:
> ```html
> <ul itemize="items">
> <template>
> <li>{{name}} <button ${{onclick: '^onItemClick'}}>Click</button></li>
> </template>
> </ul>
> ```
> Without a context prefix, the binding looks for the handler on the item itself — which doesn't have it, so it breaks.
>
> **Pattern 2: Custom `item-tag` (smart items)** — items are full components with their own class, templates, and methods.
> Handlers defined on the item component itself do **NOT** need `^`:
> ```html
> <div itemize="items" item-tag="my-item"></div>
> ```
> ```js
> class MyItem extends Symbiote {
> onItemClick() { console.log(this.$.name); }
> }
> MyItem.template = html`<li>{{name}} <button ${{onclick: 'onItemClick'}}>Click</button></li>`;
> MyItem.reg('my-item');
> ```
> Here `onItemClick` is the item's own method — no `^` needed.
> You can still use `^` to reach the parent list component if needed.
### Custom item component
```html
<div itemize="items" item-tag="my-item"></div>
```
Then define `my-item` as a separate Symbiote component with its own template, methods, and state.
### Data formats
- **Array**: `[{prop: val}, ...]` — items rendered in order
- **Object**: `{key1: {prop: val}, ...}` — items get `_KEY_` property added
### Updating lists
Assign new array to trigger re-render:
```js
this.$.items = [...newItems]; // triggers update
```
Existing items are updated in-place via `set$`, new items appended, excess removed.
---
## Slots (Light DOM)
Slots work without Shadow DOM (processed by `slotProcessor`). Import and add manually since v2.x:
```js
import { slotProcessor } from '@symbiotejs/symbiote/core/slotProcessor.js';
class MyWrapper extends Symbiote {
constructor() {
super();
this.templateProcessors.add(slotProcessor);
}
}
MyWrapper.template = html`
<header><slot name="header"></slot></header>
<main><slot></slot></main>
`;
```
Usage:
```html
<my-wrapper>
<h1 slot="header">Title</h1>
<p>Default slot content</p>
</my-wrapper>
```
---
## Server-Side Rendering (SSR)
Import `node/SSR.js` to render components to HTML strings on the server. Requires `linkedom` (optional peer dependency).
### Basic usage — `processHtml`
```js
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
await SSR.init(); // patches globals with linkedom env
await import('./my-component.js'); // component reg() works normally
let html = await SSR.processHtml('<div><my-component></my-component></div>');
// => '<div><my-component><style>...</style><template shadowrootmode="open">...</template>content</my-component></div>'
SSR.destroy(); // cleanup globals
```
`processHtml` takes any HTML string, renders all Symbiote components found within, and returns the processed HTML. If `SSR.init()` was already called, it reuses the existing environment; otherwise it auto-initializes (and auto-destroys after).
### Advanced — `renderToString` / `renderToStream`
```js
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
await SSR.init();
await import('./my-component.js');
let html = SSR.renderToString('my-component', { title: 'Hello' });
// => '<my-component title="Hello"><h1>Hello</h1></my-component>'
SSR.destroy();
```
### API
| Method | Description |
|--------|-------------|
| `SSR.init()` | `async` — creates linkedom document, polyfills CSSStyleSheet/NodeFilter/MutationObserver/adoptedStyleSheets, patches globals |
| `SSR.processHtml(html)` | `async` — parses HTML string, renders all custom elements, returns processed HTML. Auto-inits if needed |
| `SSR.renderToString(tagName, attrs?)` | Creates element, triggers `connectedCallback`, serializes to HTML string |
| `SSR.renderToStream(tagName, attrs?)` | Async generator — yields HTML chunks. Same output as `renderToString`, but streamed for lower TTFB |
| `SSR.destroy()` | Removes global patches, cleans up document |
### Styles in SSR output
- **rootStyles** → `<style>` tag as first child of the component (light DOM, deduplicated per constructor)
- **shadowStyles** → `<style>` inside the Declarative Shadow DOM `<template>`
- Both are supported simultaneously on the same component
### Streaming usage
```js
import http from 'node:http';
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
await SSR.init();
import './my-app.js';
http.createServer(async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write('<!DOCTYPE html><html><body>');
for await (let chunk of SSR.renderToStream('my-app')) {
res.write(chunk);
}
res.end('</body></html>');
}).listen(3000);
```
### Shadow DOM output
Shadow components produce Declarative Shadow DOM markup with styles inlined. Light DOM content is preserved alongside the DSD template:
```html
<my-shadow>
<style>my-shadow { display: block; }</style>
<template shadowrootmode="open">
<style>:host { color: red; }</style>
<h1>Content</h1>
<slot></slot>
</template>
Light DOM content here
</my-shadow>
```
### SSR context detection
`SSR.init()` sets `globalThis.__SYMBIOTE_SSR = true`. This is separate from the instance `ssrMode` flag:
| Flag | Scope | Purpose |
|------|-------|-------|
| `__SYMBIOTE_SSR` | Server (global) | Preserves binding attributes (`bind`, `ref`, `itemize`) in HTML output. Bypasses `ssrMode` effects |
| `ssrMode` | Client (instance) | Skips template injection, hydrates existing DOM with bindings |
### Hydration flow
1. **Server**: `SSR.processHtml()` / `SSR.renderToString()` produces HTML with `bind=` / `itemize=` attributes preserved
2. **Client**: component with `ssrMode = true` skips template injection, attaches bindings to pre-rendered DOM
3. State mutations on client update DOM reactively
---
## Routing (AppRouter)
### Path-based routing (recommended)
```js
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
const routerCtx = AppRouter.initRoutingCtx('R', {
home: { pattern: '/', title: 'Home', default: true },
user: { pattern: '/users/:id', title: 'User Profile' },
settings: { pattern: '/settings', title: 'Settings' },
notFound: { pattern: '/404', title: 'Not Found', error: true },
});
// Navigate programmatically
AppRouter.navigate('user', { id: '42' });
// URL becomes: /users/42
// React to route changes in any component
this.sub('R/route', (route) => console.log('Route:', route));
this.sub('R/options', (opts) => console.log('Params:', opts)); // { id: '42' }
```
### Query-string routing (legacy/alternative)
```js
// Routes WITHOUT `pattern` use query-string mode automatically
const routerCtx = AppRouter.initRoutingCtx('R', {
home: { title: 'Home', default: true },
about: { title: 'About' },
});
AppRouter.navigate('about', { section: 'team' });
// URL becomes: ?about§ion=team
```
### Route guards
```js
// Register guard — runs before every navigation
let unsub = AppRouter.beforeRoute((to, from) => {
if (!isAuth && to.route === 'settings') {
return 'login'; // redirect
}
// return false to cancel, nothing to proceed
});
unsub(); // remove guard
```
### Lazy loaded routes
```js
AppRouter.initRoutingCtx('R', {
settings: {
pattern: '/settings',
title: 'Settings',
load: () => import('./pages/settings.js'), // loaded once, cached
},
});
```
### AppRouter API
- `AppRouter.initRoutingCtx(ctxName, routingMap)` → PubSub
- `AppRouter.navigate(route, options?)` — navigate and dispatch event
- `AppRouter.reflect(route, options?)` — update URL without triggering event
- `AppRouter.notify()` — read URL, run guards, lazy load, dispatch event
- `AppRouter.beforeRoute(fn)` — register guard, returns unsubscribe fn
- `AppRouter.setRoutingMap(map)` — extend routes
- `AppRouter.readAddressBar()` → `{ route, options }`
- `AppRouter.setSeparator(char)` — default `&` (query-string mode)
- `AppRouter.setDefaultTitle(title)`
- `AppRouter.removePopstateListener()`
- Mode auto-detected: routes with `pattern` → path-based, without → query-string
---
## Attribute Binding
```js
class MyComponent extends Symbiote {
init$ = {
'@name': '', // reads from HTML attribute `name` automatically
};
}
MyComponent.bindAttributes({
'value': 'inputValue', // maps HTML attr `value` → state prop `inputValue`
});
// observedAttributes is auto-populated
```
---
## CSS Data Binding
Read CSS custom property values into component state:
```js
class MyComponent extends Symbiote {
cssInit$ = {
'--accent-color': '#ff0', // fallback value
};
}
```
Or in template:
```html
<div>{{--my-css-prop}}</div>
```
Update with: `this.updateCssData()` / `this.dropCssDataCache()`.
---
## Component Registration
```js
// Explicit tag name
MyComponent.reg('my-component');
// Auto-generated tag (sym-1, sym-2, ...)
MyComponent.reg();
// Alias registration (creates a subclass)
MyComponent.reg('my-alias', true);
// Get tag name (auto-registers if needed)
const tag = MyComponent.is; // 'my-component'
```
---
## Utilities
```js
import { UID } from '@symbiotejs/symbiote/utils';
UID.generate('XXXXX-XXX'); // e.g. 'aB3kD-z9Q'
import { create, applyStyles, applyAttributes } from '@symbiotejs/symbiote/utils';
let el = create({ tag: 'div', attributes: { id: 'x' }, styles: { color: 'red' }, children: [...] });
import { reassignDictionary } from '@symbiotejs/symbiote/utils';
reassignDictionary({ BIND_ATTR: 'data-bind' }); // customize internal attribute names
```
---
## Security (Trusted Types)
Template `innerHTML` writes use a Trusted Types policy when the API is available:
```js
// Symbiote creates a passthrough policy automatically:
// trustedTypes.createPolicy('symbiote', { createHTML: (s) => s })
```
This makes Symbiote compatible with strict CSP headers:
```
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types symbiote
```
No sanitization is performed — templates are developer-authored, not user input. The policy name is `'symbiote'`.
---
## Dev Mode
Enable verbose warnings during development:
```js
Symbiote.devMode = true;
```
### Dev messages module
All warning/error strings are in an optional module. Without it, warnings print short codes like `[Symbiote W5]`. Import once to get full messages and auto-enable `devMode`:
```js
import '@symbiotejs/symbiote/core/devMessages.js';
```
**Always-on** (regardless of `devMode`):
- `W1` PubSub: cannot read/publish/subscribe — property not found
- `W3` context already registered, `W4` context not found
- `W5` custom template not found, `W8` tag already registered, `W9` CSS data parse error
- `W13`/`W14` AppRouter messages, `W16` itemize data type error
- `E15` `this` in template interpolation error (`html` tag detects `${this.x}` usage)
**Dev-only** (`devMode = true`):
- `W2` type change on publish, `W6` `*prop` without ctx, `W7` shared prop conflict
- `W10` CSS data binding in SSR, `W11` unresolved binding key, `W12` text-node binding in SSR/ISO
---
## Common Mistakes to Avoid
1. **DON'T** use `this` in template strings — templates are decoupled from component context
2. **DON'T** nest property keys with dots in state: `'obj.prop'` won't work as a state key
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 `^`
4. **DON'T** use `@` prefix directly in HTML — it's only for binding syntax (`${{'@attr': 'prop'}}`)
5. **DON'T** treat `init$` as a regular object — it's processed at connection time
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`.
7. **DON'T** expect Shadow DOM by default — use `renderShadow = true` or `shadowStyles` to opt in
8. **DON'T** wrap Custom Elements in extra divs — the custom tag IS the wrapper
9. **DON'T** use CSS frameworks (Tailwind, etc.) — use native CSS with custom properties
10. **DON'T** use `require()` — ESM only (import/export)
11. **DON'T** use `*prop` without `ctx` attribute or `--ctx` CSS variable — shared context won't be created
12. **DON'T** rely on class property fallbacks for `^`-targeted properties — the `^` walk only checks the parent's data context (`init$` / `add$()`), not own class properties