Skip to content

Commit a0f1f68

Browse files
committed
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.
1 parent 33584db commit a0f1f68

45 files changed

Lines changed: 244 additions & 157 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ import { html, css, WebComponent, render, renderToString } from 'webjs';
308308
| `html` | Tagged template literal producing a `TemplateResult`. Use in pages, layouts, and component `render()`. |
309309
| `css` | Tagged template literal producing a `CSSResult`. Assign to `static styles` on components. |
310310
| `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')`. |
312312
| `render(v, el)` | Client-side: render a value into a DOM element. |
313313
| `renderToString` | Server-side: **async** — render a value to an HTML string with DSD injection. Awaits Promise-valued holes and async component `render()` methods. |
314314
| `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 {
431431
return html``;
432432
}
433433
}
434-
customElements.define('my-thing', MyThing);
434+
MyThing.register('my-thing');
435435
```
436436

437437
Mutate state with `this.setState({...})` — it batches a re-render via microtask.
@@ -456,7 +456,7 @@ class StudentCard extends WebComponent {
456456
return html`<p>${this.student.name}</p>`;
457457
}
458458
}
459-
customElements.define('student-card', StudentCard);
459+
StudentCard.register('student-card');
460460
```
461461

462462
Built-in constructors (`String`, `Number`, `Boolean`, `Array`, `Object`)
@@ -798,7 +798,7 @@ When you mark an action as `expose('METHOD /path', fn)`, you are declaring it pa
798798
799799
### Components (`components/*.js`)
800800
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.
802802
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.
803803
- Imported by pages (for SSR) and/or other components (for composition).
804804
- **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.
952952
953953
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.
954954
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')`.
956956
4. **Event (`@`), property (`.`), and boolean (`?`) holes in `html` must be unquoted** — e.g. `@click=${fn}`, never `@click="${fn}"`.
957957
5. **Do not mutate `this.state` directly** — use `setState`. State reads are fine.
958958
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';
10421042
export class HelloWorld extends WebComponent {
10431043
render() { return html`<p>Hello!</p>`; }
10441044
}
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.
10461049
10471050
### Scaffold commands
10481051

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class Counter extends WebComponent {
9999
`;
100100
}
101101
}
102-
customElements.define('my-counter', Counter);
102+
Counter.register('my-counter');
103103
```
104104

105105
Need scoped styles, `<slot>` projection, or embed-ready isolation? Opt

docs/app/docs/components/page.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class MyCounter extends WebComponent {
3535
}
3636
}
3737
38-
customElements.define('my-counter', MyCounter);</pre>
38+
MyCounter.register('my-counter');</pre>
3939
4040
<p>That is a complete, working component. Import it from a page or layout and use it like any HTML element:</p>
4141
@@ -46,14 +46,14 @@ export default function Home() {
4646
}</pre>
4747
4848
<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>&lt;my-counter&gt;</code> from built-in elements like <code>&lt;div&gt;</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>&lt;my-counter&gt;</code> from built-in elements like <code>&lt;div&gt;</code>. Register the component with <code>Class.register('tag')</code> at the bottom of the file:</p>
5050
5151
<pre>class UserCard extends WebComponent {
5252
// ...
5353
}
54-
customElements.define('user-card', UserCard);</pre>
54+
UserCard.register('user-card');</pre>
5555
56-
<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>
5757
5858
<h2>Properties</h2>
5959
<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>
@@ -83,7 +83,7 @@ customElements.define('user-card', UserCard);</pre>
8383
\`;
8484
}
8585
}
86-
customElements.define('user-card', UserCard);</pre>
86+
UserCard.register('user-card');</pre>
8787
8888
<h3>Attribute-to-Property Coercion</h3>
8989
<p>When an attribute changes on the DOM element, webjs coerces the string value to the declared type:</p>
@@ -143,7 +143,7 @@ customElements.define('user-card', UserCard);</pre>
143143
\`;
144144
}
145145
}
146-
customElements.define('todo-list', TodoList);</pre>
146+
TodoList.register('todo-list');</pre>
147147
148148
<h3>How setState Works</h3>
149149
<ul>
@@ -185,7 +185,7 @@ class StyledCard extends WebComponent {
185185
\`;
186186
}
187187
}
188-
customElements.define('styled-card', StyledCard);</pre>
188+
StyledCard.register('styled-card');</pre>
189189
190190
<h3>How Styles Are Applied</h3>
191191
<ul>
@@ -238,7 +238,7 @@ static styles = css\`
238238
\`;
239239
}
240240
}
241-
customElements.define('app-card', AppCard);</pre>
241+
AppCard.register('app-card');</pre>
242242
243243
<h3>Class-prefix rule for custom CSS</h3>
244244
<p>Tailwind utilities are unique by construction, so most light-DOM components need zero custom CSS. If you <em>do</em> author a <code>&lt;style&gt;</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 {
291291
\`;
292292
}
293293
}
294-
customElements.define('my-card', Card);
294+
Card.register('my-card');
295295
296296
<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>
297297
@@ -361,7 +361,7 @@ html\`
361361
\`;
362362
}
363363
}
364-
customElements.define('page-layout', PageLayout);
364+
PageLayout.register('page-layout');
365365
366366
// Usage: assign content to named slots with the slot="" attribute
367367
html\`
@@ -481,10 +481,10 @@ render() {
481481
482482
<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>
483483
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>
486486
487-
<pre>customElements.define('my-counter', MyCounter);</pre>
487+
<pre>MyCounter.register('my-counter');</pre>
488488
489489
<p>webjs wraps the native API (and installs a compatible shim on the server) so the same line works in both environments:</p>
490490
<ul>
@@ -494,7 +494,7 @@ render() {
494494
495495
<p>Module URLs for <code>&lt;link rel="modulepreload"&gt;</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>
496496
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>
498498
499499
<h2>Server Rendering</h2>
500500
<p>webjs components are server-rendered using <strong>Declarative Shadow DOM</strong>. When the server renders a page containing <code>&lt;my-counter count="5"&gt;&lt;/my-counter&gt;</code>, the output looks like:</p>
@@ -513,7 +513,7 @@ render() {
513513
514514
<h3>How SSR Works</h3>
515515
<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>
517517
<li>During <code>renderToString()</code>, the server scans the output HTML for registered custom element tags.</li>
518518
<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>&lt;template shadowrootmode="open"&gt;</code> block with the component's styles.</li>
519519
<li>The browser parses this as a native declarative shadow root — the content is visible <strong>before any JavaScript loads</strong>.</li>
@@ -537,7 +537,7 @@ render() {
537537
\`;
538538
}
539539
}
540-
customElements.define('user-profile', UserProfile);</pre>
540+
UserProfile.register('user-profile');</pre>
541541
542542
<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>
543543
@@ -605,7 +605,7 @@ class TaskList extends WebComponent {
605605
\`;
606606
}
607607
}
608-
customElements.define('task-list', TaskList);</pre>
608+
TaskList.register('task-list');</pre>
609609
610610
<h3>How repeat() Works</h3>
611611
<ul>
@@ -683,13 +683,13 @@ class ChatBox extends WebComponent {
683683
\`;
684684
}
685685
}
686-
customElements.define('chat-box', ChatBox);</pre>
686+
ChatBox.register('chat-box');</pre>
687687
688688
<h2>Quick Reference</h2>
689689
<ul>
690690
<li><strong>Extend</strong> <code>WebComponent</code> and set <code>static tag</code>, <code>static properties</code>, <code>static styles</code>.</li>
691691
<li><strong>Implement</strong> <code>render()</code> returning <code>html\`...\`</code>.</li>
692-
<li><strong>Register</strong> with <code>customElements.define('tag', ClassName)</code>.</li>
692+
<li><strong>Register</strong> with <code>ClassName.register('tag')</code>.</li>
693693
<li><strong>State</strong> — use <code>this.setState({...})</code> for shallow merge + batched re-render.</li>
694694
<li><strong>Events</strong><code>@click</code>, <code>@submit</code>, <code>@input</code> in templates. Stable dispatchers, no listener churn.</li>
695695
<li><strong>Bindings</strong><code>attr=\${v}</code> for attributes, <code>.prop=\${v}</code> for properties, <code>?bool=\${v}</code> for booleans.</li>

docs/app/docs/context/page.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class AppShell extends WebComponent {
7070
\`;
7171
}
7272
}
73-
customElements.define('app-shell', AppShell);</pre>
73+
AppShell.register('app-shell');</pre>
7474
7575
<p>When you call <code>provider.setValue(newValue)</code>, every subscribed consumer is notified and its host component re-renders automatically.</p>
7676
@@ -104,7 +104,7 @@ class ThemedCard extends WebComponent {
104104
\`;
105105
}
106106
}
107-
customElements.define('themed-card', ThemedCard);</pre>
107+
ThemedCard.register('themed-card');</pre>
108108
109109
<h2>Subscribe vs One-Shot Mode</h2>
110110
<p>The <code>subscribe</code> option controls whether the consumer receives ongoing updates:</p>
@@ -175,7 +175,7 @@ class AppRoot extends WebComponent {
175175
return html\`&lt;slot&gt;&lt;/slot&gt;\`;
176176
}
177177
}
178-
customElements.define('app-root', AppRoot);</pre>
178+
AppRoot.register('app-root');</pre>
179179
180180
<h2>Nested Providers</h2>
181181
<p>Providers can be nested. A consumer resolves to the nearest ancestor provider with a matching context key:</p>
@@ -226,7 +226,7 @@ class AuthProvider extends WebComponent {
226226
return html\`&lt;slot&gt;&lt;/slot&gt;\`;
227227
}
228228
}
229-
customElements.define('auth-provider', AuthProvider);
229+
AuthProvider.register('auth-provider');
230230
231231
// components/user-menu.ts
232232
import { WebComponent, html } from 'webjs';
@@ -247,7 +247,7 @@ class UserMenu extends WebComponent {
247247
return html\`&lt;span&gt;Hi, \${user.name}&lt;/span&gt;\`;
248248
}
249249
}
250-
customElements.define('user-menu', UserMenu);</pre>
250+
UserMenu.register('user-menu');</pre>
251251
252252
<h2>Next Steps</h2>
253253
<ul>

docs/app/docs/controllers/page.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class LazyImage extends WebComponent {
7878
\`;
7979
}
8080
}
81-
customElements.define('lazy-image', LazyImage);</pre>
81+
LazyImage.register('lazy-image');</pre>
8282
8383
<h2>Example: FetchController</h2>
8484
<p>A reusable controller that fetches data from a URL and exposes loading/error/data states:</p>
@@ -135,7 +135,7 @@ class UserList extends WebComponent {
135135
\`;
136136
}
137137
}
138-
customElements.define('user-list', UserList);</pre>
138+
UserList.register('user-list');</pre>
139139
140140
<h2>Multiple Controllers on One Component</h2>
141141
<p>Controllers compose naturally. A single component can use any number of controllers:</p>
@@ -156,7 +156,7 @@ customElements.define('user-list', UserList);</pre>
156156
return html\`&lt;div&gt;\${this.#data.data?.summary}&lt;/div&gt;\`;
157157
}
158158
}
159-
customElements.define('dashboard-widget', DashboardWidget);</pre>
159+
DashboardWidget.register('dashboard-widget');</pre>
160160
161161
<h2>Built-in Controllers</h2>
162162
<p>webjs ships three controllers out of the box:</p>

docs/app/docs/conventions/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function Conventions() {
1313
<ul>
1414
<li><strong>Module architecture</strong> — where actions, queries, and components go.</li>
1515
<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>
1717
<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>
1818
<li><strong>Server action patterns</strong> — one function per file, <code>ActionResult</code> envelope.</li>
1919
<li><strong>Code style</strong> — TypeScript extensions, const/let preferences, async/await patterns.</li>
@@ -52,7 +52,7 @@ webjs check --rules</pre>
5252
<ul>
5353
<li><strong>Actions in modules</strong> — server actions live under <code>modules/&lt;feature&gt;/actions/</code>, not scattered in random directories.</li>
5454
<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>
5656
<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>
5757
<li><strong>Tests exist for modules</strong> — every module under <code>modules/</code> has corresponding test files.</li>
5858
<li><strong>Tag names have hyphens</strong> — custom element tags contain at least one hyphen (HTML spec requirement).</li>

docs/app/docs/editor-setup/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class StudentCard extends WebComponent {
5656
return html\`&lt;p&gt;\${this.student.name}&lt;/p&gt;\`;
5757
}
5858
}
59-
customElements.define('student-card', StudentCard);</pre>
59+
StudentCard.register('student-card');</pre>
6060
6161
<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>
6262

docs/app/docs/getting-started/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class Counter extends WebComponent {
108108
\`;
109109
}
110110
}
111-
customElements.define('my-counter', Counter);</pre>
111+
Counter.register('my-counter');</pre>
112112
113113
<h3>Run it</h3>
114114
<pre>npx webjs dev

docs/app/docs/lazy-loading/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class HeavyChart extends WebComponent {
3434
return html${'`'}&lt;canvas&gt;&lt;/canvas&gt;${'`'};
3535
}
3636
}
37-
customElements.define('heavy-chart', HeavyChart);</pre>
37+
HeavyChart.register('heavy-chart');</pre>
3838
3939
<h2>How it works</h2>
4040
<ol>

docs/app/docs/server-actions/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class PostForm extends WebComponent {
111111
\`;
112112
}
113113
}
114-
customElements.define('post-form', PostForm);</pre>
114+
PostForm.register('post-form');</pre>
115115
116116
<h2>The superjson Wire Format</h2>
117117
<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 {
321321
\`;
322322
}
323323
}
324-
customElements.define('todo-app', TodoApp);
324+
TodoApp.register('todo-app');
325325
326326
// 3. Call the REST endpoint from curl
327327
// curl -X POST http://localhost:3000/api/todos \\

0 commit comments

Comments
 (0)