You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): Counter.register('my-counter') as the idiomatic registration
Adds a `static register(tag)` method on WebComponent. Delegates to the
internal registry (which in turn calls customElements.define / the
server shim), so every registration still lands in the native
customElements map and webjs's mirror map. Authors write:
class Counter extends WebComponent { ... }
Counter.register('my-counter');
Instead of:
customElements.define('my-counter', Counter);
~12 characters shorter, class-first reading order.
customElements.define remains supported — both patterns work
interchangeably; the scanner, plugin, and lint rule all accept either.
scanner / plugin / lint
-----------------------
• component-scanner: extractor finds both Class.register('tag') AND
customElements.define('tag', Class) patterns. Orphan detection
accepts either as registration.
• webjs-plugin: getDefinitionAndBoundSpan resolves tags from both
patterns; existing tests updated accordingly.
• check.js: rule renamed components-have-define →
components-have-register. Accepts either pattern.
tag-name-has-hyphen validates tag literals from both.
• Dev server orphan warning (unchanged wording) fires when neither
pattern is present for a WebComponent subclass.
migration
---------
All 11 blog components, scaffold ThemeToggle template, 15+ doc pages,
AGENTS.md, README.md, CONVENTIONS.md swept via regex
`customElements.define\\('tag', Class\\)` → `Class.register('tag')`.
tests
-----
• test/check.test.js: 2 fixtures now cover both patterns (Class.register
vs customElements.define), confirming the rule accepts either.
• test/component-scanner.test.js + test/webjs-plugin.test.js already
covered both patterns via the extractor / resolver updates.
Tests: 269 → 270 unit. Browser: 21.
|`html`| Tagged template literal producing a `TemplateResult`. Use in pages, layouts, and component `render()`. |
309
309
|`css`| Tagged template literal producing a `CSSResult`. Assign to `static styles` on components. |
310
310
|`WebComponent`| Base class for interactive components. |
311
-
|`register(tag,C)`| Register a tag → class. Called automatically by `customElements.define('tag', Class)`. |
311
+
|`register(tag,C)`| Register a tag → class. Called automatically by `Class.register('tag')`. |
312
312
|`render(v, el)`| Client-side: render a value into a DOM element. |
313
313
|`renderToString`| Server-side: **async** — render a value to an HTML string with DSD injection. Awaits Promise-valued holes and async component `render()` methods. |
314
314
|`notFound()`| Throw inside a page/layout/server action to return a 404 rendered via `not-found.js`. |
@@ -431,7 +431,7 @@ class MyThing extends WebComponent {
431
431
return html`…`;
432
432
}
433
433
}
434
-
customElements.define('my-thing', MyThing);
434
+
MyThing.register('my-thing');
435
435
```
436
436
437
437
Mutate state with`this.setState({...})` — it batches a re-render via microtask.
@@ -456,7 +456,7 @@ class StudentCard extends WebComponent {
@@ -798,7 +798,7 @@ When you mark an action as `expose('METHOD /path', fn)`, you are declaring it pa
798
798
799
799
### Components (`components/*.js`)
800
800
801
-
- Each file should define **one** custom element and call `customElements.define('tag', Class)` at module top level.
801
+
- Each file should define **one** custom element and call `Class.register('tag')` at module top level.
802
802
Passing `import.meta.url` lets the SSR shell emit a `<link rel="modulepreload">` so the browser can fetch the module without waiting for its parent to parse. Zero build step; big first-paint win.
803
803
- Imported by pages (for SSR) and/or other components (for composition).
804
804
- **Styling convention: shadow-DOM CSS via `static styles = css\`…\``, not inline `style="…"` attributes.** Any repeated visual chunk in pages (layout chrome, cards, muted labels, etc.) should become a component whose styles live in its shadow root. The example app's `<blog-shell>` and `<muted-text>` demonstrate this — pages emit semantic HTML with zero inline styles.
@@ -952,7 +952,7 @@ the class-prefix rule documented in the Shadow-vs-Light DOM section.
952
952
953
953
1. **Never import `@prisma/client`, `node:*`, or any server-only dependency from a file under `components/` or from a page's top-level module graph that isn't a server action.** The browser will try to load it and fail. Use a server action instead.
954
954
2. **Every `*.server.js` export must be an `async` JSON-safe function.** Arguments/results are serialised over the wire.
955
-
3. **Custom element tag names must contain a hyphen** (HTML spec). Set `static tag`, call `customElements.define('tag', Class)`.
955
+
3. **Custom element tag names must contain a hyphen** (HTML spec). Set `static tag`, call `Class.register('tag')`.
956
956
4. **Event (`@`), property (`.`), and boolean (`?`) holes in `html` must be unquoted** — e.g. `@click=${fn}`, never `@click="${fn}"`.
957
957
5. **Do not mutate `this.state` directly** — use `setState`. State reads are fine.
958
958
6. **Page and layout default exports must be functions.** They return a value (usually a `TemplateResult`); they do not call `render()` themselves.
@@ -1042,7 +1042,10 @@ import { WebComponent, html } from 'webjs';
1042
1042
exportclassHelloWorldextendsWebComponent {
1043
1043
render() { returnhtml`<p>Hello!</p>`; }
1044
1044
}
1045
-
customElements.define('hello-wcustomElements.define('my-customElements.define('my-card', HelloWorld);Then use it as `<hello-world></hello-world>`in any page or component.
1045
+
HelloWorld.register('hello-world');
1046
+
```
1047
+
1048
+
Then use it as `<hello-world></hello-world>` in any page or component.
<p>That is a complete, working component. Import it from a page or layout and use it like any HTML element:</p>
41
41
@@ -46,14 +46,14 @@ export default function Home() {
46
46
}</pre>
47
47
48
48
<h2>Tag Names</h2>
49
-
<p>The HTML spec requires that custom element names contain a <strong>hyphen</strong>. This is how the browser distinguishes <code><my-counter></code> from built-in elements like <code><div></code>. Register the component with the standard <code>customElements.define()</code> API at the bottom of the file:</p>
49
+
<p>The HTML spec requires that custom element names contain a <strong>hyphen</strong>. This is how the browser distinguishes <code><my-counter></code> from built-in elements like <code><div></code>. Register the component with <code>Class.register('tag')</code> at the bottom of the file:</p>
<p>If you forget the hyphen, the browser throws at <code>customElements.define</code> time with a clear error message.</p>
56
+
<p>If you forget the hyphen, the browser throws at registration time with a clear error message.</p>
57
57
58
58
<h2>Properties</h2>
59
59
<p>The <code>static properties</code> object declares which HTML attributes the component observes, along with their type for coercion. The browser's <code>observedAttributes</code> list is auto-derived from the property names — you never write it by hand.</p>
<p>Tailwind utilities are unique by construction, so most light-DOM components need zero custom CSS. If you <em>do</em> author a <code><style></code> block or import a stylesheet, <strong>every class selector MUST be prefixed with the component's tag name</strong>. Otherwise two components with a <code>.card</code> or <code>.header</code> class will style each other.</p>
@@ -291,7 +291,7 @@ class MyCard extends WebComponent {
291
291
\`;
292
292
}
293
293
}
294
-
customElements.define('my-card', Card);
294
+
Card.register('my-card');
295
295
296
296
<p><code>static styles</code> on a light-DOM component is silently ignored — there's no shadow root to adopt them into. If you see your styles failing, check whether you forgot <code>static shadow = true</code>.</p>
297
297
@@ -361,7 +361,7 @@ html\`
361
361
\`;
362
362
}
363
363
}
364
-
customElements.define('page-layout', PageLayout);
364
+
PageLayout.register('page-layout');
365
365
366
366
// Usage: assign content to named slots with the slot="" attribute
367
367
html\`
@@ -481,10 +481,10 @@ render() {
481
481
482
482
<p>During SSR, <code>?disabled=\${true}</code> emits <code>disabled=""</code> and <code>?disabled=\${false}</code> emits nothing — matching how the browser interprets boolean attributes.</p>
483
483
484
-
<h2>customElements.define()</h2>
485
-
<p>Register the component with the web-standard <code>customElements.define()</code> API at the bottom of the file:</p>
484
+
<h2>Class.register('tag')</h2>
485
+
<p>Register the component with <code>Class.register('tag')</code> at the bottom of the file:</p>
<p>webjs wraps the native API (and installs a compatible shim on the server) so the same line works in both environments:</p>
490
490
<ul>
@@ -494,7 +494,7 @@ render() {
494
494
495
495
<p>Module URLs for <code><link rel="modulepreload"></code> hints are discovered separately, by a server-side scanner that walks the app tree at boot and derives the file path for each discovered tag. No per-component <code>import.meta.url</code> argument needed.</p>
496
496
497
-
<blockquote>Always call <code>customElements.define</code> at the module's top level, outside the class body. The component registers as soon as the module is imported, both on server and client.</blockquote>
497
+
<blockquote>Always call <code>Class.register</code> at the module's top level, outside the class body. The component registers as soon as the module is imported, both on server and client.</blockquote>
498
498
499
499
<h2>Server Rendering</h2>
500
500
<p>webjs components are server-rendered using <strong>Declarative Shadow DOM</strong>. When the server renders a page containing <code><my-counter count="5"></my-counter></code>, the output looks like:</p>
@@ -513,7 +513,7 @@ render() {
513
513
514
514
<h3>How SSR Works</h3>
515
515
<ul>
516
-
<li>The server imports the component module, which calls <code>customElements.define()</code> and stores the class in the registry.</li>
516
+
<li>The server imports the component module, which calls <code>Class.register('tag')</code> and stores the class in the registry.</li>
517
517
<li>During <code>renderToString()</code>, the server scans the output HTML for registered custom element tags.</li>
518
518
<li>For each match, it creates a temporary instance, applies attributes from the HTML, calls <code>render()</code>, and wraps the result in a <code><template shadowrootmode="open"></code> block with the component's styles.</li>
519
519
<li>The browser parses this as a native declarative shadow root — the content is visible <strong>before any JavaScript loads</strong>.</li>
<p>On the client, <code>render()</code> is called synchronously. If you need async data on the client, fetch it in <code>connectedCallback()</code> and call <code>setState()</code> when the data arrives.</p>
543
543
@@ -605,7 +605,7 @@ class TaskList extends WebComponent {
<li><strong>Register</strong> with <code>customElements.define('tag', ClassName)</code>.</li>
692
+
<li><strong>Register</strong> with <code>ClassName.register('tag')</code>.</li>
693
693
<li><strong>State</strong> — use <code>this.setState({...})</code> for shallow merge + batched re-render.</li>
694
694
<li><strong>Events</strong> — <code>@click</code>, <code>@submit</code>, <code>@input</code> in templates. Stable dispatchers, no listener churn.</li>
695
695
<li><strong>Bindings</strong> — <code>attr=\${v}</code> for attributes, <code>.prop=\${v}</code> for properties, <code>?bool=\${v}</code> for booleans.</li>
Copy file name to clipboardExpand all lines: docs/app/docs/conventions/page.ts
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,7 @@ export default function Conventions() {
13
13
<ul>
14
14
<li><strong>Module architecture</strong> — where actions, queries, and components go.</li>
15
15
<li><strong>Testing rules</strong> — when unit vs E2E tests are required.</li>
16
-
<li><strong>Component patterns</strong> — light DOM by default with Tailwind; shadow DOM opt-in; <code>customElements.define()</code>; the class-prefix rule for light-DOM custom CSS.</li>
16
+
<li><strong>Component patterns</strong> — light DOM by default with Tailwind; shadow DOM opt-in; <code>Class.register('tag')</code>; the class-prefix rule for light-DOM custom CSS.</li>
17
17
<li><strong>Styling convention</strong> — Tailwind browser runtime + <code>@theme</code> tokens; JS helpers in <code>app/_utils/ui.ts</code> to dedupe repeated class bundles; no <code>@apply</code>.</li>
18
18
<li><strong>Server action patterns</strong> — one function per file, <code>ActionResult</code> envelope.</li>
<li><strong>Actions in modules</strong> — server actions live under <code>modules/<feature>/actions/</code>, not scattered in random directories.</li>
54
54
<li><strong>One function per action</strong> — each <code>.server.ts</code> file exports a single named async function.</li>
55
-
<li><strong>Components have register()</strong> — every component class calls <code>customElements.define()</code> at module top level.</li>
55
+
<li><strong>Components have register()</strong> — every component class calls <code>Class.register('tag')</code> at module top level.</li>
56
56
<li><strong>No server imports in client code</strong> — <code>@prisma/client</code>, <code>node:*</code>, and other server-only modules are not imported from components or pages.</li>
57
57
<li><strong>Tests exist for modules</strong> — every module under <code>modules/</code> has corresponding test files.</li>
58
58
<li><strong>Tag names have hyphens</strong> — custom element tags contain at least one hyphen (HTML spec requirement).</li>
<p>Inside the class, <code>this.student</code> is a real <code>Student</code> — hover, autocomplete, type-checking all work. <code>this.setState</code>, <code>this.state</code>, <code>this.requestUpdate</code>, and all lifecycle hooks are typed by the framework's <code>.d.ts</code> overlay.</p>
<p>Server actions use <strong>superjson</strong> for serialisation, not plain <code>JSON.stringify</code>. The content type is <code>application/vnd.webjs+json</code>. This means rich JavaScript types survive the round trip:</p>
@@ -321,7 +321,7 @@ export class TodoApp extends WebComponent {
321
321
\`;
322
322
}
323
323
}
324
-
customElements.define('todo-app', TodoApp);
324
+
TodoApp.register('todo-app');
325
325
326
326
// 3. Call the REST endpoint from curl
327
327
// curl -X POST http://localhost:3000/api/todos \\
0 commit comments