Skip to content

Commit 7b48653

Browse files
authored
docs: align cem-plugin example with the typed-props pattern
1 parent 4965231 commit 7b48653

8 files changed

Lines changed: 62 additions & 41 deletions

File tree

demo/examples/typed-props/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
<body>
1818
<h1>Compile-time prop types</h1>
1919
<p>
20-
Pass the shape of your defaults as a type argument —
21-
<code>extends WebComponent&lt;typeof props&gt;</code> and
22-
<code>this.props.*</code> is inferred from them instead of being
20+
Declare the shape as a named type and pass it as the class type
21+
argument — <code>extends WebComponent&lt;TypedButtonProps&gt;</code>
22+
and <code>this.props.*</code> is checked against it instead of being
2323
<code>any</code>.
2424
</p>
2525

docs/src/content/docs/api/cem-plugin.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ initializer and, for every key, records in the manifest:
5151
Without it the analyzer sees `props` as one opaque static field and emits no
5252
attributes, so editor completion and Storybook controls have nothing to read.
5353

54-
The `static props` object may be declared inline or as an identifier resolved
55-
from the same source file.
54+
The `static props` initializer must resolve to an object literal in the same
55+
source file.

docs/src/content/docs/api/web-component.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ In TypeScript, pass the shape of `static props` as a type argument to get a
2020
typed `this.props`:
2121

2222
```ts
23-
const props = { variant: 'primary', disabled: false }
23+
type CozyButtonProps = {
24+
variant: 'primary' | 'ghost'
25+
disabled: boolean
26+
}
2427

25-
class CozyButton extends WebComponent<typeof props> {
26-
static props = props
28+
class CozyButton extends WebComponent<CozyButtonProps> {
29+
static props: CozyButtonProps = {
30+
variant: 'primary',
31+
disabled: false,
32+
}
2733
}
2834
```
2935

docs/src/content/docs/guides/cem-plugin.mdx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ npm i -D @custom-elements-manifest/analyzer
3737
import { wcbStaticProps } from 'web-component-base/cem-plugin'
3838

3939
export default {
40-
globs: ['src/**/*.js'],
40+
globs: ['src/**/*.{js,ts}'],
4141
outdir: '.',
4242
plugins: [wcbStaticProps()],
4343
}
@@ -66,11 +66,19 @@ With globs scoped to your source it should finish in well under a second.
6666

6767
Given a component:
6868

69-
```js
70-
const props = { variant: 'primary', disabled: false, maxCount: 3 }
69+
```ts
70+
type CozyButtonProps = {
71+
variant: 'primary' | 'ghost'
72+
disabled: boolean
73+
maxCount: number
74+
}
7175

72-
export class CozyButton extends WebComponent {
73-
static props = props
76+
export class CozyButton extends WebComponent<CozyButtonProps> {
77+
static props: CozyButtonProps = {
78+
variant: 'primary',
79+
disabled: false,
80+
maxCount: 3,
81+
}
7482
static shadowRootInit = { mode: 'open' }
7583
static styles = ':host { display: inline-block }'
7684
get template() {
@@ -92,11 +100,9 @@ customElements.define('cozy-button', CozyButton)
92100

93101
Two details worth knowing:
94102

95-
- **Types come from the default literal**: `true`/`false``boolean`, numeric → `number`, object/array → `object`, everything else → `string`.
103+
- **Types come from the default literal**: `true`/`false``boolean`, numeric → `number`, object/array → `object`, everything else → `string`. The TypeScript annotation is not consulted, so `variant`'s union still lands in the manifest as `string`.
96104
- **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.
97105

98-
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.
99-
100106
## Storybook
101107

102108
Storybook's web-components renderer builds **autodocs and controls** from a [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest).
@@ -118,19 +124,37 @@ Bind a story to the tag name and Storybook infers the rest, giving a text field
118124
```js
119125
// cozy-button.stories.js
120126
import { html } from 'lit'
121-
import '../src/cozy-button.js'
127+
import '../src/cozy-button.ts'
122128

123129
export default {
124130
title: 'Cozy/Button',
125131
component: 'cozy-button', // ← no argTypes needed
126-
render: ({ variant, disabled }) => html`
127-
<cozy-button variant=${variant} disabled=${disabled}></cozy-button>
132+
render: ({ variant, disabled, maxCount }) => html`
133+
<cozy-button
134+
variant=${variant}
135+
?disabled=${disabled}
136+
max-count=${maxCount}
137+
></cozy-button>
128138
`,
129139
}
130140

131-
export const Default = { args: { variant: 'primary', disabled: false } }
141+
export const Default = {
142+
args: { variant: 'primary', disabled: false, maxCount: 3 },
143+
}
132144
```
133145

146+
<Aside type="note" title="The `html` in story files is lit's, not wcb's">
147+
Storybook's web-components renderer renders stories with
148+
[lit-html](https://lit.dev/docs/templates/overview/), so story files import
149+
`html` from `lit` and templates in them use **lit's binding syntax** — the
150+
`?disabled=${disabled}` prefix above is lit for "add or remove the boolean
151+
attribute" (a plain `disabled=${false}` would write `disabled="false"`,
152+
which wcb [parses as `true`](/prop-access/#boolean-props)). None of this
153+
applies to your component: it keeps wcb's own `html` tag with no special
154+
binding prefixes, and lit is a devDependency of the Storybook setup, never
155+
shipped with the component.
156+
</Aside>
157+
134158
<Aside type="tip">
135159
Regenerate the manifest before starting Storybook (`cem analyze && storybook
136160
dev`). `custom-elements.json` is a build artifact, so it is usually
@@ -170,7 +194,7 @@ import { wcbStaticProps } from 'web-component-base/cem-plugin'
170194
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator'
171195

172196
export default {
173-
globs: ['src/**/*.js'],
197+
globs: ['src/**/*.{js,ts}'],
174198
outdir: '.',
175199
plugins: [wcbStaticProps(), generateCustomData()],
176200
}

docs/src/content/docs/guides/prop-access.mdx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,14 @@ el.toggleAttribute('flag', true) // prop syncs, component re-renders
9898
Enumerated attributes like `contenteditable` and `aria-*` attributes are the
9999
exception. They are genuine strings where `"false"` is meaningful, so declare
100100
them as **string** props rather than booleans. Strings serialize literally and
101-
are never removed, so they need nothing special at runtime; in TypeScript you
102-
can narrow them to the values you accept:
101+
are never removed, so they need nothing special at runtime; in TypeScript,
102+
narrow them to the values you accept in your
103+
[props type](#typed-props-in-typescript):
103104

104105
```ts
105-
const props = { ariaChecked: 'false' as 'true' | 'false' }
106+
type ToggleProps = {
107+
ariaChecked: 'true' | 'false'
108+
}
106109
```
107110
108111
<Aside type="tip" title="Default boolean props to false">
@@ -285,15 +288,6 @@ default outside the union is a compile error too.
285288

286289
This is types-only. The runtime is unchanged, and `static strictProps` still guards writes that come in from attributes at runtime. Omitting the type argument keeps the previous untyped behavior.
287290

288-
<Aside type="tip">
289-
You can also infer the type from the defaults instead of writing it out:
290-
declare them in a `const` and pass its `typeof`:
291-
`class CozyButton extends WebComponent<typeof props> { static props = props }`.
292-
Inferred values widen (`variant: 'primary'` types as plain `string`), so
293-
narrow with a cast where it matters:
294-
`variant: 'primary' as 'primary' | 'ghost'`.
295-
</Aside>
296-
297291
### Unobserved properties
298292

299293
Everything declared in `static props` is observed and reflected: each key gets

src/cem-plugin.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,13 @@ function unwrap(ts, node) {
7575

7676
/**
7777
* Resolves an identifier to the object literal of a module-level `const` in
78-
* the same file. This is what makes the typed-props pattern work:
78+
* the same file, so a component that keeps its defaults in a shared `const`
79+
* still yields attributes instead of silently emitting none:
7980
*
8081
* ```js
8182
* const props = { variant: 'primary' }
8283
* class Foo extends WebComponent { static props = props }
8384
* ```
84-
*
85-
* A class can't reference its own static in its `extends` clause, so typed
86-
* components must hoist the defaults into a const — without this the whole
87-
* pattern would yield zero attributes.
8885
* @param {any} ts the TypeScript module handed to the hook
8986
* @param {any} node any node in the source file
9087
* @param {string} name the identifier to resolve

storybook/stories/prop-types.stories.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const BooleanProps = {
1515
args: { isInline: false, anotherone: false },
1616
render: ({ isInline, anotherone }) => html`
1717
<boolean-prop-test
18-
is-inline=${isInline}
19-
anotherone=${anotherone}
18+
?is-inline=${isInline}
19+
?anotherone=${anotherone}
2020
></boolean-prop-test>
2121
`,
2222
}

storybook/stories/typed-button.stories.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default {
1010
render: ({ variant, disabled, clicks }) => html`
1111
<typed-button
1212
variant=${variant}
13-
disabled=${disabled}
13+
?disabled=${disabled}
1414
clicks=${clicks}
1515
></typed-button>
1616
`,

0 commit comments

Comments
 (0)