From cef76dfd660ab979b0987ae91f9994fbdd5c1d25 Mon Sep 17 00:00:00 2001 From: ayo Date: Wed, 22 Jul 2026 01:18:01 +0200 Subject: [PATCH] docs: tighten the "why" page (grammar and figures) --- docs/src/content/docs/api/cem-plugin.md | 2 +- docs/src/content/docs/api/html.md | 4 +- docs/src/content/docs/api/utils.md | 4 +- docs/src/content/docs/api/web-component.md | 24 ++++----- docs/src/content/docs/guides/cem-plugin.mdx | 40 +++++++------- docs/src/content/docs/guides/comparison.md | 10 ++-- docs/src/content/docs/guides/examples.md | 2 +- .../content/docs/guides/getting-started.mdx | 10 ++-- .../content/docs/guides/life-cycle-hooks.md | 14 ++--- docs/src/content/docs/guides/prop-access.mdx | 54 +++++++++---------- docs/src/content/docs/guides/styling.md | 6 +-- .../content/docs/guides/template-vs-render.md | 4 +- docs/src/content/docs/guides/why.md | 18 ++++--- 13 files changed, 97 insertions(+), 95 deletions(-) diff --git a/docs/src/content/docs/api/cem-plugin.md b/docs/src/content/docs/api/cem-plugin.md index 5fab5cd..422e98f 100644 --- a/docs/src/content/docs/api/cem-plugin.md +++ b/docs/src/content/docs/api/cem-plugin.md @@ -44,7 +44,7 @@ initializer and, for every key, records in the manifest: - the **member** under its camelCase name - the **attribute** under its kebab-case name, linked to that member -- the **type** inferred from the default value — `boolean`, `number`, `object` +- the **type** inferred from the default value: `boolean`, `number`, `object` or `string` - the **default value** as written diff --git a/docs/src/content/docs/api/html.md b/docs/src/content/docs/api/html.md index 77412b1..ff352fb 100644 --- a/docs/src/content/docs/api/html.md +++ b/docs/src/content/docs/api/html.md @@ -48,7 +48,7 @@ html`

hi

` Because the tree is a plain object it is comparable and serializable, which is what lets `render()` diff one render against the next. -`` html`` `` returns `undefined` — this is the idiomatic way for a component to +`` html`` `` returns `undefined`. This is the idiomatic way for a component to render nothing, and it empties the rendered subtree rather than leaving the previous render on screen. @@ -60,7 +60,7 @@ Each entry in `props` is applied by [`applyProp`](/api/utils/#applypropel-prop-v in this order: 1. a `style` object is applied rule by rule -2. a name the element owns as a **DOM property** is assigned to that property — +2. a name the element owns as a **DOM property** is assigned to that property, so event handlers (`onclick=${fn}`) and non-string values keep their type 3. a boolean value with no matching DOM property is toggled as an HTML boolean attribute diff --git a/docs/src/content/docs/api/utils.md b/docs/src/content/docs/api/utils.md index e4f7c28..009a222 100644 --- a/docs/src/content/docs/api/utils.md +++ b/docs/src/content/docs/api/utils.md @@ -63,7 +63,7 @@ everything else pass through unchanged. ### `deserialize(value, type)` -Parses an attribute string back into a value of the given declared type — the +Parses an attribute string back into a value of the given declared type, the inverse of `serialize()`. | Parameter | Type | | @@ -72,7 +72,7 @@ inverse of `serialize()`. | `type` | `string` | `'boolean'`, `'number'`, `'object'`, `'undefined'` or `'string'` | | **returns** | `any` | the parsed value | -`'boolean'` always returns `true` — strict HTML boolean-attribute semantics, +`'boolean'` always returns `true`: strict HTML boolean-attribute semantics, where any present value is true. Absence is handled by the caller and never reaches here. `'number'`, `'object'` and `'undefined'` use `JSON.parse` and throw on malformed input; strings pass through. diff --git a/docs/src/content/docs/api/web-component.md b/docs/src/content/docs/api/web-component.md index 6d27b57..bdfd28b 100644 --- a/docs/src/content/docs/api/web-component.md +++ b/docs/src/content/docs/api/web-component.md @@ -1,7 +1,7 @@ --- title: WebComponent slug: api/web-component -description: The WebComponent base class — static configuration, instance members, lifecycle hooks and attribute converters. +description: 'The WebComponent base class: static configuration, instance members, lifecycle hooks and attribute converters.' --- The base class every component extends. Import from the package root or its own @@ -56,8 +56,8 @@ reported with `console.warn`: | Default | Warning | | ------------------ | --------------------------------------------------- | -| a function or symbol | not reflectable — use handlers or refs instead | -| `true` | boolean defaults should be `false` — invert the name | +| a function or symbol | not reflectable: use handlers or refs instead | +| `true` | boolean defaults should be `false`: invert the name | A `true` boolean default is discouraged because HTML has no true-by-default boolean attribute: absence would have to mean both "false" and "default". Name @@ -80,7 +80,7 @@ per-component rules after it. Strings are compiled to a `CSSStyleSheet` once; existing `CSSStyleSheet` instances are adopted as-is and can be shared across components. -Adoption happens **once per instance**, when the element is constructed — not +Adoption happens **once per instance**, when the element is constructed, not per render. Requires [`shadowRootInit`](#static-shadowrootinit). Without a shadow root @@ -92,7 +92,7 @@ See it live: [Constructable styles demo ↗](https://demo.webcomponent.io/exampl ### `static shadowRootInit` A [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options) -object. Its presence is what opts the component into shadow DOM — the shadow +object. Its presence is what opts the component into shadow DOM. The shadow root is attached during construction and becomes the render target. ```js @@ -143,9 +143,9 @@ Assigning the value it already holds does nothing. Read-only getter returning what the component renders. Two kinds are supported: -- an [`html`](/api/html/) tagged template — a vnode tree, reconciled in place +- an [`html`](/api/html/) tagged template: a vnode tree, reconciled in place on re-render -- a **string** — assigned to the render target's `innerHTML` +- a **string**: assigned to the render target's `innerHTML` Both render into the same target: the shadow root when `shadowRootInit` is set, the element itself otherwise. Returning `` html`` `` (which is `undefined`) or @@ -165,7 +165,7 @@ Renders `template` into the render target. Called automatically on connect and on every prop or attribute change; you rarely call it yourself. For a vnode template, the new tree is compared against the previous one and -re-render **patches the existing DOM in place** — see +re-render **patches the existing DOM in place**. See [Template vs Render](/template-vs-render/) for what that preserves and the non-keyed matching caveat. @@ -202,7 +202,7 @@ Override these to control how one prop crosses the prop/attribute boundary, and call `super` for the props you do not handle. The default conversion round-trips values through JSON. Types JSON cannot -restore — `Date`, `Map`, `Set`, `URL`, class instances — need overridden +restore (`Date`, `Map`, `Set`, `URL`, class instances) need overridden converters to live on `static props`; see [Custom attribute conversion](/prop-access/#custom-attribute-conversion) for worked examples, including the non-serializable cases. @@ -229,7 +229,7 @@ toAttribute(name, value) { ### `fromAttribute(name, value)` -Converts an attribute value into the prop value it represents — the inverse of +Converts an attribute value into the prop value it represents, the inverse of `toAttribute()`. | Parameter | Type | Description | @@ -256,7 +256,7 @@ Boolean props follow the HTML convention in both directions: **presence means | `true` | present, empty value | `''` | | `false` | absent | `null` | -Any present value reads as `true` — including the literal `flag="false"`, just +Any present value reads as `true`, including the literal `flag="false"`, just as native `disabled="false"` is still disabled. Removing the attribute always yields `false`, never the declared default. @@ -265,7 +265,7 @@ Use `toggleAttribute(name, bool)` to set them. Writing when it sees a boolean attribute written as `"true"` or `"false"` so the inversion cannot fail silently. -Attributes whose `"false"` is meaningful — `aria-*`, `contenteditable` — should +Attributes whose `"false"` is meaningful (`aria-*`, `contenteditable`) should be declared as **string** props. See it live: [Boolean props demo ↗](https://demo.webcomponent.io/examples/boolean-props/) diff --git a/docs/src/content/docs/guides/cem-plugin.mdx b/docs/src/content/docs/guides/cem-plugin.mdx index d4b2fba..5027ba9 100644 --- a/docs/src/content/docs/guides/cem-plugin.mdx +++ b/docs/src/content/docs/guides/cem-plugin.mdx @@ -6,7 +6,7 @@ slug: cem-plugin import { Aside } from '@astrojs/starlight/components' A CEM (`custom-elements.json`) is a standard description of the elements a -package defines — their tags, attributes, properties, events and slots — so +package defines (their tags, attributes, properties, events and slots) so tooling can read your components without executing them. Read more at [custom-elements-manifest.open-wc.org](https://custom-elements-manifest.open-wc.org/) for the analyzer and its plugin API, or the [schema and @@ -20,7 +20,7 @@ We provide a CEM Analyzer Plugin `web-component-base/cem-plugin` which allows us set up: a starter component, this plugin configured in `custom-elements-manifest.config.mjs`, an `analyze` script, and the `customElements` field set in `package.json`. The rest of this guide is the - manual path — use it to add the manifest to an existing project, or to + manual path: use it to add the manifest to an existing project, or to understand what the scaffold configured. @@ -53,10 +53,10 @@ npx cem analyze Run it **from the directory holding your config and a `package.json`**. Two things make it appear to do nothing, neither of which prints an error: -- **No config found** — it falls back to a default glob of +- **No config found**: it falls back to a default glob of `**/*.{js,ts,tsx}`, which does _not_ exclude `node_modules`. In a real project that is tens of thousands of files through the TypeScript parser. -- **No `package.json` in the directory** — it hangs outright. +- **No `package.json` in the directory**: it hangs outright. With globs scoped to your source it should finish in well under a second. @@ -92,10 +92,10 @@ customElements.define('cozy-button', CozyButton) Two details worth knowing: -- **Types come from the default literal** — `true`/`false` → `boolean`, numeric → `number`, object/array → `object`, everything else → `string`. +- **Types come from the default literal**: `true`/`false` → `boolean`, numeric → `number`, object/array → `object`, everything else → `string`. - **Attribute names come from wcb's own `getKebabCase`**, the same function `observedAttributes` uses, so manifest names can't drift from what the component actually observes. -The defaults may be written inline or hoisted into a module-level `const` — the latter is required by the [typed props](/prop-access/#typed-props-in-typescript) pattern, and the plugin resolves it either way. +The defaults may be written inline or hoisted into a module-level `const`. The latter is required by the [typed props](/prop-access/#typed-props-in-typescript) pattern, and the plugin resolves it either way. ## Storybook @@ -113,7 +113,7 @@ setCustomElementsManifest(manifest) export default { tags: ['autodocs'] } ``` -Bind a story to the tag name and Storybook infers the rest — a text field for `variant`, a toggle for `disabled`, a number input for `maxCount`: +Bind a story to the tag name and Storybook infers the rest, giving a text field for `variant`, a toggle for `disabled`, a number input for `maxCount`: ```js // cozy-button.stories.js @@ -133,19 +133,19 @@ export const Default = { args: { variant: 'primary', disabled: false } } -This repo runs exactly this setup against the demo components in `storybook/` — see it there for a working reference. +This repo runs exactly this setup against the demo components in `storybook/`. See it there for a working reference. ## Code editors -Once `custom-elements.json` exists, editors can offer tag-name and attribute autocomplete for your components — driven by the same `static props` the plugin reads, so the hints can't drift from the code. +Once `custom-elements.json` exists, editors can offer tag-name and attribute autocomplete for your components, driven by the same `static props` the plugin reads, so the hints can't drift from the code. First, point tooling at the manifest from your `package.json`. This is the field every option here uses to discover it: @@ -156,7 +156,7 @@ First, point tooling at the manifest from your `package.json`. This is the field } ``` -### Route 1 — native VS Code, no extension +### Route 1: native VS Code, no extension VS Code's built-in HTML language service reads its own [custom data](https://github.com/microsoft/vscode-custom-data) format. A second analyzer plugin converts the manifest into it, so both files come out of one `cem analyze` run: @@ -187,25 +187,25 @@ export default { `html.customData` paths resolve from the **workspace root**, not from the settings file. If you run `cem analyze` in a subfolder, either point at `./that-folder/vscode.html-custom-data.json` or give the generator its own - `outdir` — `generateCustomData({ outdir: '..' })` — so the file lands where + `outdir`, `generateCustomData({ outdir: '..' })`, so the file lands where the setting expects it. Restart VS Code and ` This corner of the ecosystem moves quickly and every option is pre-1.0. Route - 1 is the conservative choice — it is plain VS Code configuration with no - extension to go stale — at the cost of not covering tagged templates. + 1 is the conservative choice (it is plain VS Code configuration with no + extension to go stale) at the cost of not covering tagged templates. diff --git a/docs/src/content/docs/guides/comparison.md b/docs/src/content/docs/guides/comparison.md index 312a8b3..1f201b1 100644 --- a/docs/src/content/docs/guides/comparison.md +++ b/docs/src/content/docs/guides/comparison.md @@ -17,13 +17,13 @@ Numbers below are **measured** from the same minimal counter component (one reac | `@elenajs/core` | 1.0.0 | 9.1 kB | 3.7 kB | 3.3 kB | | `lit` | 3.3.3 | 15.3 kB | 5.9 kB | 5.3 kB | | `@microsoft/fast-element` | 3.0.1 | 44.9 kB | 13.7 kB | 12.2 kB | -| vanilla `HTMLElement` | — | — | — | ~0.2 kB | +| vanilla `HTMLElement` | - | - | - | ~0.2 kB | For scale: even after all of the v5.2–v6.1 work, the WCB counter is **~21% smaller than Elena, ~51% smaller than Lit, and ~79% smaller than FAST**. ## Feature comparison -What each library gives you beyond extending directly from `HTMLElement` — the boilerplate you no longer write by hand: +What each library gives you beyond extending directly from `HTMLElement`, the boilerplate you no longer write by hand: | Capability | WCB 6.1 | Lit 3.3 | Elena 1.0 | FAST 3.0 | | -------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- | --------------------------------------- | ------------------------------------------------ | @@ -42,11 +42,11 @@ What each library gives you beyond extending directly from `HTMLElement` — the | Backing / ecosystem | solo maintainer, small surface | Google, huge ecosystem | new (2026), design-system focus | Microsoft, powers Fluent UI | :::note[Why 11ty WebC isn't here] -WebC is a compile-time tool: it resolves components during an Eleventy build and ships plain HTML with no client runtime. Every row above is about what a library does _in the browser at runtime_, so a side-by-side comparison would be measuring two different things. If your components are static at build time, WebC solves a different problem — and solves it well. +WebC is a compile-time tool: it resolves components during an Eleventy build and ships plain HTML with no client runtime. Every row above is about what a library does _in the browser at runtime_, so a side-by-side comparison would be measuring two different things. If your components are static at build time, WebC solves a different problem, and solves it well. ::: -For what these numbers and capabilities add up to — and when they don't — see [Why would anyone use WCB?](/why/). +For what these numbers and capabilities add up to (and when they don't) see [Why would anyone use WCB?](/why/). --- -_WCB re-measured 2026-07-20 at v6.1.0; the other libraries measured 2026-07-19, with esbuild, Node zlib (gzip −9, brotli q11), at the pinned versions above. Methodology: identical counter component per library, bundled per library, compressed. Re-run them yourself — the benchmark is trivially reproducible with the versions pinned above._ +_WCB re-measured 2026-07-20 at v6.1.0; the other libraries measured 2026-07-19, with esbuild, Node zlib (gzip −9, brotli q11), at the pinned versions above. Methodology: identical counter component per library, bundled per library, compressed. Re-run them yourself. The benchmark is trivially reproducible with the versions pinned above._ diff --git a/docs/src/content/docs/guides/examples.md b/docs/src/content/docs/guides/examples.md index 28ee48c..502ee1f 100644 --- a/docs/src/content/docs/guides/examples.md +++ b/docs/src/content/docs/guides/examples.md @@ -6,7 +6,7 @@ slug: examples ## Live demo gallery Every example below runs as a standalone page at -[demo.webcomponent.io ↗](https://demo.webcomponent.io/) — a live gallery with +[demo.webcomponent.io ↗](https://demo.webcomponent.io/), a live gallery with the source alongside each demo. | Demo | Shows | diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index e5a1e25..cb5b8e8 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -20,9 +20,9 @@ npm create wcb@latest It sets up a publishable custom element, ready to develop: -- **A starter component** in TypeScript using the [`static props`](/prop-access/) convention — the class, tag, and file names are derived from your project name (`my-button` → `MyButton` / ``), so there are no rename-me TODOs -- **A Vite setup** — a dev server with a demo page, plus a library build (`npm run build:lib`) that emits ESM + UMD bundles and `.d.ts` types, with `web-component-base` as a peerDependency -- **`custom-elements.json` generation** — the CEM analyzer configured with wcb's plugin, an `npm run analyze` script, the `customElements` field in `package.json`, and a `prepack` hook so the manifest ships inside the published package +- **A starter component** in TypeScript using the [`static props`](/prop-access/) convention. The class, tag, and file names are derived from your project name (`my-button` → `MyButton` / ``), so there are no rename-me TODOs +- **A Vite setup**: a dev server with a demo page, plus a library build (`npm run build:lib`) that emits ESM + UMD bundles and `.d.ts` types, with `web-component-base` as a peerDependency +- **`custom-elements.json` generation**: the CEM analyzer configured with wcb's plugin, an `npm run analyze` script, the `customElements` field in `package.json`, and a `prepack` hook so the manifest ships inside the published package Run the demo page to see the starter component render: @@ -32,7 +32,7 @@ npm install npm run dev ``` -Vite prints a local URL — open it to see `` rendered on the demo page. Edit `src/MyButton.ts` and the page updates as you save. +Vite prints a local URL. Open it to see `` rendered on the demo page. Edit `src/MyButton.ts` and the page updates as you save. See the [CEM Analyzer Plugin guide](/cem-plugin/) for more details on the manifest and how to use it with Storybook and code editors. @@ -57,7 +57,7 @@ import { WebComponent } from 'web-component-base' Continue with [Usage](/usage) to define your first component. :::tip[Prefer a CDN?] -You can also import wcb straight from a CDN in vanilla JS or HTML files — see the [CodePen examples](/examples/#codepen-examples) for working setups. +You can also import wcb straight from a CDN in vanilla JS or HTML files. See the [CodePen examples](/examples/#codepen-examples) for working setups. ::: ## Getting help diff --git a/docs/src/content/docs/guides/life-cycle-hooks.md b/docs/src/content/docs/guides/life-cycle-hooks.md index a7c97ee..40013ab 100644 --- a/docs/src/content/docs/guides/life-cycle-hooks.md +++ b/docs/src/content/docs/guides/life-cycle-hooks.md @@ -77,9 +77,9 @@ class ClickableText extends WebComponent { - Triggered when an attribute value changed - The `changes` object cleanly separates the **property** from the **attribute**: - - `property` — the **camelCase** prop key, matching how you access `props` (e.g. `myName`) - - `attribute` — the **kebab-case** attribute name that changed (e.g. `my-name`) - - `previousValue` / `currentValue` — the values before and after the change + - `property`: the **camelCase** prop key, matching how you access `props` (e.g. `myName`) + - `attribute`: the **kebab-case** attribute name that changed (e.g. `my-name`) + - `previousValue` / `currentValue`: the values before and after the change Use `property` to read the value straight off `props` (`this.props[property]`); use `attribute` when you need the raw attribute name. @@ -117,7 +117,7 @@ If you previously read `changes.property` for the attribute name, switch to `cha ## Upgrade ordering & the buffering guarantee -Per the Custom Elements spec, when an element is upgraded with attributes already present in the markup (e.g. ``), the browser fires `attributeChangedCallback` **before** `connectedCallback`. Taken literally, that means `render()` and `onChanges()` could run before `onInit()` — so any setup you do in `onInit` (event wiring, reading external state) would not have happened yet on that first render. Test environments like happy-dom/jsdom don't reproduce this ordering, so components can pass in tests and then misbehave in a real browser. +Per the Custom Elements spec, when an element is upgraded with attributes already present in the markup (e.g. ``), the browser fires `attributeChangedCallback` **before** `connectedCallback`. Taken literally, that means `render()` and `onChanges()` could run before `onInit()`, so any setup you do in `onInit` (event wiring, reading external state) would not have happened yet on that first render. Test environments like happy-dom/jsdom don't reproduce this ordering, so components can pass in tests and then misbehave in a real browser. `WebComponent` removes this footgun. Attribute changes that arrive **before** the element is connected are buffered: @@ -126,8 +126,8 @@ Per the Custom Elements spec, when an element is upgraded with attributes alread On connect, the order is always: -1. `onInit()` — `this.props` already reflects any authored attributes -2. a single `render()` — reflects all buffered props in one pass +1. `onInit()`: `this.props` already reflects any authored attributes +2. a single `render()`: reflects all buffered props in one pass 3. `afterViewInit()` -**`onChanges()` never fires before `onInit()`.** Pre-connect attribute changes are **not** replayed through `onChanges()` — the first `render()` already reflects them, so `onChanges()` is reserved for genuine post-connect changes. After the element is connected, attribute changes behave normally: each one triggers `render()` and `onChanges()` immediately. +**`onChanges()` never fires before `onInit()`.** Pre-connect attribute changes are **not** replayed through `onChanges()`. The first `render()` already reflects them, so `onChanges()` is reserved for genuine post-connect changes. After the element is connected, attribute changes behave normally: each one triggers `render()` and `onChanges()` immediately. diff --git a/docs/src/content/docs/guides/prop-access.mdx b/docs/src/content/docs/guides/prop-access.mdx index 660eee8..1541ce7 100644 --- a/docs/src/content/docs/guides/prop-access.mdx +++ b/docs/src/content/docs/guides/prop-access.mdx @@ -65,7 +65,7 @@ class FlagBox extends WebComponent { ``` -Reflection works the same way in reverse — `true` sets the bare attribute, +Reflection works the same way in reverse. `true` sets the bare attribute, `false` removes it entirely: ```js @@ -88,7 +88,7 @@ el.toggleAttribute('flag', true) // prop syncs, component re-renders Enumerated attributes like `contenteditable` and `aria-*` attributes are the -exception — they are genuine strings where `"false"` is meaningful, so declare +exception. They are genuine strings where `"false"` is meaningful, so declare them as **string** props rather than booleans. Strings serialize literally and are never removed, so they need nothing special at runtime; in TypeScript you can narrow them to the values you accept: @@ -109,7 +109,7 @@ const props = { ariaChecked: 'false' as 'true' | 'false' } HTML has no true-default boolean attribute: absence has to mean both "false" and "default", which only works when they coincide. Model an on-by-default flag with an inverted name (`disabled`, not `enabled`). wcb warns once per - class on a `true` default — and for such a prop, removing the attribute lands + class on a `true` default, and for such a prop, removing the attribute lands on `false`, not back on the declared default. @@ -118,8 +118,8 @@ const props = { ariaChecked: 'false' as 'true' | 'false' } See it live: [Custom attribute converters demo ↗](https://demo.webcomponent.io/examples/attribute-converters/) The rules above cover the common cases. When a prop needs its own -serialization — a `Date`, a delimited list, an enumerated attribute where -`"false"` is meaningful — override `toAttribute` and `fromAttribute`, and +serialization (a `Date`, a delimited list, an enumerated attribute where +`"false"` is meaningful) override `toAttribute` and `fromAttribute`, and delegate everything else to `super`: ```js @@ -144,7 +144,7 @@ class EventCard extends WebComponent { ``` Both take the **camelCase prop key**, matching your `static props` declaration -and `onChanges`'s `property` — not the kebab-case attribute name. +and `onChanges`'s `property`, not the kebab-case attribute name. `toAttribute` returning **`null` removes the attribute**. That is exactly how a `false` boolean becomes an absent attribute, and it is available to any prop: @@ -158,7 +158,7 @@ toAttribute(name, value) {