Skip to content

Add @b9g/crank/web-components (CrankHTMLElement) - #363

Open
brainkim wants to merge 1 commit into
mainfrom
web-components
Open

Add @b9g/crank/web-components (CrankHTMLElement)#363
brainkim wants to merge 1 commit into
mainfrom
web-components

Conversation

@brainkim

@brainkim brainkim commented Jun 22, 2026

Copy link
Copy Markdown
Member

Implements the class-based CrankHTMLElement from the RFC in #360 (closes #47).

What's implemented

  • Static config: observedAttributes, events, formAssociated, shadowDOM, styles.
  • render method (this = element, ctx = arg), all four component forms via a stable bridge.
  • requestUpdate() — microtask-batched, coalescing, safe before first connect.
  • Light DOM (default, render owns the subtree) or shadow DOM with native <slot>.
  • Faithful on<type> handlers (lazy activate/deactivate; reassignment preserves listener order), typed from CrankHTMLElement<{events: typeof Class.events}> (names-only for a tuple, typed payloads for an event map).
  • static styles derived once per class (string / CSSStyleSheet / mixed array), adopted into the shadow root or document.
  • Form association: base re-renders on the four form callbacks; ElementInternals stays the author's.
  • Pre-upgrade properties re-applied through their accessors on connect.
  • Render errors → cancelable error event, then reportError if unhandled.
  • DOM renderer widened to accept a ShadowRoot (or document fragment) as a render root.

Examples — examples/web-components.tsx

The RFC's Scope examples: x-blink (generator + timer, light DOM), x-marquee (shadow DOM, scoped static styles, imperative start()/stop(), typed static events), and x-rating (form-associated, ElementInternals value/validity, super-able reset/restore), plus a small demo wiring them together.

Tests — test/web-components.tsx, 33 passing in Chromium

Behavior: sync/async/generator/async-generator render; attribute re-render + same-value bail; requestUpdate + coalescing; generator-state persistence; shadow + slot projection; open ShadowRootInit and closed mode; on<type> firing + listener ordering; in-component error fallback; render-error event (sync + async) + reportError; form FormData/reset/:disabled; light + shadow style adoption (deduped, mixed arrays); pre-upgrade property upgrade; light-DOM child replacement; move-vs-removal teardown.
Integration: rendering a custom element through Crank's own DOM renderer; nested custom elements; ctx.refresh/schedule/after; provisions within an element; computed shadow styles; accessor→attribute reflection. Plus a compile-only check of the event typing (tuple and event-map forms).
Full suite green (606 passing); tsc --noEmit clean; build emits the ./web-components export (.js/.cjs/.d.ts).

Bugs the tests caught and this PR fixes: light DOM wasn't replacing authored children; a pre-connect requestUpdate fired a spurious extra render; pre-upgrade properties weren't upgraded.

Out of scope (deferred by the RFC)

SSR / declarative shadow DOM / hydration (RFC Appendix), the form-value SSR shim, customized built-ins, and a scoped css prop.

🤖 Generated with Claude Code

@brainkim
brainkim marked this pull request as ready for review June 22, 2026 23:25
@brainkim brainkim mentioned this pull request Jun 24, 2026
7 tasks
@brainkim
brainkim force-pushed the web-components branch 2 times, most recently from 6f77ca6 to 3a9738f Compare July 14, 2026 04:54
Comment thread package.json
"./umd.js": {
"require": "./dist/umd.js"
},
"./web-components": {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on shortening this to @b9g/crank/wc?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every other crank subpath is a spelled-out word (/standalone, /dom, /html, /jsx-runtime), so /web-components matches the family; /wc would be the lone abbreviation and less greppable. I lean /web-components, but the shorter import is a trivial change if you prefer it.

Adds a `CrankHTMLElement` base class for defining custom elements whose
shadow content is rendered by Crank, so Crank components can be published
as framework-agnostic web components.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web Components: Discussion about how to use Crank.js for custom elements

1 participant