UNPKG

@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
# 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&section=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