Skip to content

Commit 1cea431

Browse files
authored
Corrected and improved docs on type definitions for Custom Elements. (#3073)
Corrected and improved documentation on type definitions for Custom Elements. The doc was incorrect for two reasons: - For Vue-based custom elements, the types passed into `GlobalComponents` need to be the Vue component types, not the custom element types, or the custom elements will not be type checked in Vue templates. - The name of the elements need to have at least two words and a hyphen, otherwise with single words the custom elements will not work at all.
1 parent 6e1090c commit 1cea431

File tree

1 file changed

+259
-11
lines changed

1 file changed

+259
-11
lines changed

src/guide/extras/web-components.md

+259-11
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ If the custom elements will be used in an application that is also using Vue, yo
220220
It is recommended to export the individual element constructors to give your users the flexibility to import them on-demand and register them with desired tag names. You can also export a convenience function to automatically register all elements. Here's an example entry point of a Vue custom element library:
221221

222222
```js
223+
// elements.js
224+
223225
import { defineCustomElement } from 'vue'
224226
import Foo from './MyFoo.ce.vue'
225227
import Bar from './MyBar.ce.vue'
@@ -236,31 +238,277 @@ export function register() {
236238
}
237239
```
238240

239-
If you have many components, you can also leverage build tool features such as Vite's [glob import](https://vitejs.dev/guide/features.html#glob-import) or webpack's [`require.context`](https://webpack.js.org/guides/dependency-management/#requirecontext) to load all components from a directory.
241+
A consumer can use the elements in a Vue file,
242+
243+
```vue
244+
<script setup>
245+
import { register } from 'path/to/elements.js'
246+
register()
247+
</script>
248+
249+
<template>
250+
<my-foo ...>
251+
<my-bar ...></my-bar>
252+
</my-foo>
253+
</template>
254+
```
255+
256+
or in any other framework such as one with JSX, and with custom names:
240257

241-
### Web Components and TypeScript {#web-components-and-typescript}
258+
```jsx
259+
import { MyFoo, MyBar } from 'path/to/elements.js'
260+
261+
customElements.define('some-foo', MyFoo)
262+
customElements.define('some-bar', MyBar)
263+
264+
export function MyComponent() {
265+
return <>
266+
<some-foo ...>
267+
<some-bar ...></some-bar>
268+
</some-foo>
269+
</>
270+
}
271+
```
242272

243-
If you are developing an application or a library, you may want to [type check](/guide/scaling-up/tooling.html#typescript) your Vue components, including those that are defined as custom elements.
273+
### Vue-based Web Components and TypeScript {#web-components-and-typescript}
244274

245-
Custom elements are registered globally using native APIs, so by default they won't have type inference when used in Vue templates. To provide type support for Vue components registered as custom elements, we can register global component typings using the the [`GlobalComponents` interface](https://github.com/vuejs/language-tools/blob/master/packages/vscode-vue/README.md#usage) in Vue templates and/or in [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements):
275+
When writing Vue SFC templates, you may want to [type check](/guide/scaling-up/tooling.html#typescript) your Vue components, including those that are defined as custom elements.
276+
277+
Custom elements are registered globally in browsers using their built-in APIs, and by default they won't have type inference when used in Vue templates. To provide type support for Vue components registered as custom elements, we can register global component typings by augmenting the [`GlobalComponents` interface](https://github.com/vuejs/language-tools/blob/master/packages/vscode-vue/README.md#usage) for type checking in Vue templates (JSX users can augment the [JSX.IntrinsicElements](https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements) type instead, which is not shown here).
278+
279+
Here is how to define the type for a custom element made with Vue:
246280

247281
```typescript
248282
import { defineCustomElement } from 'vue'
249283

250-
// vue SFC
251-
import CounterSFC from './src/components/counter.ce.vue'
284+
// Import the Vue component.
285+
import SomeComponent from './src/components/SomeComponent.ce.vue'
286+
287+
// Turn the Vue component into a Custom Element class.
288+
export const SomeElement = defineCustomElement(SomeComponent)
289+
290+
// Remember to register the element class with the browser.
291+
customElements.define('some-element', SomeElement)
292+
293+
// Add the new element type to Vue's GlobalComponents type.
294+
declare module 'vue' {
295+
interface GlobalComponents {
296+
// Be sure to pass in the Vue component type here (SomeComponent, *not* SomeElement).
297+
// Custom Elements require a hyphen in their name, so use the hyphenated element name here.
298+
'some-element': typeof SomeComponent
299+
}
300+
}
301+
```
302+
303+
## Non-Vue Web Components and TypeScript
304+
305+
Here is the recommended way to enable type checking in SFC templates of Custom
306+
Elements that are not built with Vue.
307+
308+
> [!Note]
309+
> This approach is one possible way to do it, but it may vary depending on the
310+
> framework being used to create the custom elements.
311+
312+
Suppose we have a custom element with some JS properties and events defined, and
313+
it is shipped in a library called `some-lib`:
314+
315+
```ts
316+
// file: some-lib/src/SomeElement.ts
317+
318+
// Define a class with typed JS properties.
319+
export class SomeElement extends HTMLElement {
320+
foo: number = 123
321+
bar: string = 'blah'
322+
323+
lorem: boolean = false
324+
325+
// This method should not be exposed to template types.
326+
someMethod() {
327+
/* ... */
328+
}
329+
330+
// ... implementation details omitted ...
331+
// ... assume the element dispatches events named "apple-fell" ...
332+
}
333+
334+
customElements.define('some-element', SomeElement)
335+
336+
// This is a list of properties of SomeElement that will be selected for type
337+
// checking in framework templates (f.e. Vue SFC templates). Any other
338+
// properties will not be exposed.
339+
export type SomeElementAttributes = 'foo' | 'bar'
340+
341+
// Define the event types that SomeElement dispatches.
342+
export type SomeElementEvents = {
343+
'apple-fell': AppleFellEvent
344+
}
345+
346+
export class AppleFellEvent extends Event {
347+
/* ... details omitted ... */
348+
}
349+
```
350+
351+
The implementation details have been omitted, but the important part is that we
352+
have type definitions for two things: prop types and event types.
353+
354+
Let's create a type helper for easily registering custom element type
355+
definitions in Vue:
356+
357+
```ts
358+
// file: some-lib/src/DefineCustomElement.ts
359+
360+
// We can re-use this type helper per each element we need to define.
361+
type DefineCustomElement<
362+
ElementType extends HTMLElement,
363+
Events extends EventMap = {},
364+
SelectedAttributes extends keyof ElementType = keyof ElementType
365+
> = new () => ElementType & {
366+
// Use $props to define the properties exposed to template type checking. Vue
367+
// specifically reads prop definitions from the `$props` type. Note that we
368+
// combine the element's props with the global HTML props and Vue's special
369+
// props.
370+
/** @deprecated Do not use the $props property on a Custom Element ref, this is for template prop types only. */
371+
$props: HTMLAttributes &
372+
Partial<Pick<ElementType, SelectedAttributes>> &
373+
PublicProps
374+
375+
// Use $emit to specifically define event types. Vue specifically reads event
376+
// types from the `$emit` type. Note that `$emit` expects a particular format
377+
// that we map `Events` to.
378+
/** @deprecated Do not use the $emit property on a Custom Element ref, this is for template prop types only. */
379+
$emit: VueEmit<Events>
380+
}
381+
382+
type EventMap = {
383+
[event: string]: Event
384+
}
385+
386+
// This maps an EventMap to the format that Vue's $emit type expects.
387+
type VueEmit<T extends EventMap> = EmitFn<{
388+
[K in keyof T]: (event: T[K]) => void
389+
}>
390+
```
391+
392+
> [!Note]
393+
> We marked `$props` and `$emit` as deprecated so that when we get a `ref` to a
394+
> custom element we will not be tempted to use these properties, as these
395+
> properties are for type checking purposes only when it comes to custom elements.
396+
> These properties do not actually exist on the custom element instances.
397+
398+
Using the type helper we can now select the JS properties that should be exposed
399+
for type checking in Vue templates:
400+
401+
```ts
402+
// file: some-lib/src/SomeElement.vue.ts
403+
404+
import {
405+
SomeElement,
406+
SomeElementAttributes,
407+
SomeElementEvents
408+
} from './SomeElement.js'
409+
import type { Component } from 'vue'
410+
import type { DefineCustomElement } from './DefineCustomElement'
411+
412+
// Add the new element type to Vue's GlobalComponents type.
413+
declare module 'vue' {
414+
interface GlobalComponents {
415+
'some-element': DefineCustomElement<
416+
SomeElement,
417+
SomeElementAttributes,
418+
SomeElementEvents
419+
>
420+
}
421+
}
422+
```
252423

253-
// turn component into web components
254-
export const Counter = defineCustomElement(CounterSFC)
424+
Suppose that `some-lib` builds its source TypeScript files into a `dist/` folder. A user of
425+
`some-lib` can then import `SomeElement` and use it in a Vue SFC like so:
255426

256-
// register global typings
427+
```vue
428+
<script setup lang="ts">
429+
// This will create and register the element with the browser.
430+
import 'some-lib/dist/SomeElement.js'
431+
432+
// A user that is using TypeScript and Vue should additionally import the
433+
// Vue-specific type definition (users of other frameworks may import other
434+
// framework-specific type definitions).
435+
import type {} from 'some-lib/dist/SomeElement.vue.js'
436+
437+
import { useTemplateRef, onMounted } from 'vue'
438+
439+
const el = useTemplateRef('el')
440+
441+
onMounted(() => {
442+
console.log(
443+
el.value!.foo,
444+
el.value!.bar,
445+
el.value!.lorem,
446+
el.value!.someMethod()
447+
)
448+
449+
// Do not use these props, they are `undefined` (IDE will show them crossed out):
450+
el.$props
451+
el.$emit
452+
})
453+
</script>
454+
455+
<template>
456+
<!-- Now we can use the element, with type checking: -->
457+
<some-element
458+
ref="el"
459+
:foo="456"
460+
:blah="'hello'"
461+
@apple-fell="
462+
(event) => {
463+
// The type of `event` is inferred here to be `AppleFellEvent`
464+
}
465+
"
466+
></some-element>
467+
</template>
468+
```
469+
470+
If an element does not have type definitions, the types of the properties and events can be
471+
defined in a more manual fashion:
472+
473+
```vue
474+
<script setup lang="ts">
475+
// Suppose that `some-lib` is plain JS without type defintions, and TypeScript
476+
// cannot infer the types:
477+
import { SomeElement } from 'some-lib'
478+
479+
// We'll use the same type helper as before.
480+
import { DefineCustomElement } from './DefineCustomElement'
481+
482+
type SomeElementProps = { foo?: number; bar?: string }
483+
type SomeElementEvents = { 'apple-fell': AppleFellEvent }
484+
interface AppleFellEvent extends Event {
485+
/* ... */
486+
}
487+
488+
// Add the new element type to Vue's GlobalComponents type.
257489
declare module 'vue' {
258-
export interface GlobalComponents {
259-
Counter: typeof Counter
490+
interface GlobalComponents {
491+
'some-element': DefineCustomElement<
492+
SomeElementProps,
493+
SomeElementEvents
494+
>
260495
}
261496
}
497+
498+
// ... same as before, use a reference to the element ...
499+
</script>
500+
501+
<template>
502+
<!-- ... same as before, use the element in the template ... -->
503+
</template>
262504
```
263505

506+
Custom Element authors should not automatically export framework-specific custom
507+
element type definitions from their libraries, for example they should not
508+
export them from an `index.ts` file that also exports the rest of the library,
509+
otherwise users will have unexpected module augmentation errors. Users should
510+
import the framework-specific type definition file that they need.
511+
264512
## Web Components vs. Vue Components {#web-components-vs-vue-components}
265513

266514
Some developers believe that framework-proprietary component models should be avoided, and that exclusively using Custom Elements makes an application "future-proof". Here we will try to explain why we believe that this is an overly simplistic take on the problem.

0 commit comments

Comments
 (0)