Skip to content

Commit

Permalink
Corrected and improved documentation on type definitions for Custom E…
Browse files Browse the repository at this point in the history
…lements.

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.
  • Loading branch information
trusktr committed Oct 27, 2024
1 parent 78d61f2 commit acac193
Showing 1 changed file with 271 additions and 11 deletions.
282 changes: 271 additions & 11 deletions src/guide/extras/web-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ If the custom elements will be used in an application that is also using Vue, yo
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:

```js
// elements.js
import { defineCustomElement } from 'vue'
import Foo from './MyFoo.ce.vue'
import Bar from './MyBar.ce.vue'
Expand All @@ -236,31 +238,289 @@ export function register() {
}
```

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.
A consumer can use the elements in a Vue file,

```vue
<script setup>
import { register } from 'path/to/elements.js'
register()
</script>
<template>
<my-foo ...>
<my-bar ...></my-bar>
</my-foo>
</template>
```

or in any other framework such as one with JSX, and with custom names:

```jsx
import { MyFoo, MyBar } from 'path/to/elements.js'
customElements.define('some-foo', MyFoo)
customElements.define('some-bar', MyBar)
export function MyComponent() {
return <>
<some-foo ...>
<some-bar ...></some-bar>
</some-foo>
</>
}
```

### Vue-based Web Components and TypeScript {#web-components-and-typescript}

### Web Components and TypeScript {#web-components-and-typescript}
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.

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.
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).
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):
Here is how to define the type for a custom element made with Vue:
```typescript
import { defineCustomElement } from 'vue'
// vue SFC
import CounterSFC from './src/components/counter.ce.vue'
// Import the Vue component.
import SomeComponent from './src/components/SomeComponent.ce.vue'
// Turn the Vue component into a Custom Element class.
export const SomeElement = defineCustomElement(SomeComponent)
// turn component into web components
export const Counter = defineCustomElement(CounterSFC)
// Remember to register the element class with the browser.
customElements.define('some-element', SomeElement)
// register global typings
// Add the new element type to Vue's GlobalComponents type.
declare module 'vue' {
export interface GlobalComponents {
Counter: typeof Counter
interface GlobalComponents {
// Be sure to pass in the Vue component type here (SomeComponent, *not* SomeElement).
// Custom Elements require a hyphen in their name, so use the hyphenated element name here.
'some-element': typeof SomeComponent
}
}
```
## non-Vue Web Components and TypeScript
Here is the recommended way to define types for any custom elements that are not
built with Vue (for example third-party custom elements written in other
frameworks).
> [!Note]
> People who may want to do this may be writing custom elements for use across
> frameworks, and are not using Vue for the implementation of the custom elements,
> but they want the custom elements to be type checked within Vue SFC templates.
> [!Note]
> This approach is one possible way to do it, but it may vary depending on the
> framework being used to create the custom elements. Namely, the part that will
> be important is the use of `$props` and `$emit` in the following example.
Suppose we have a custom element with some JS properties and events defined, and
it is shipped in a library called `some-lib`:
```ts
// file: some-lib/src/SomeElement.ts

// Define a class with typed JS properties (or depending on the method of
// creating elements, we at least need a type definition, not necessarily a
// class).
export class SomeElement extends HTMLElement {
foo: number = 123
bar: string = 'blah'

lorem: boolean = false

// This method should not be exposed to template types.
someMethod() {
/* ... */
}

// ... implementation details omitted ...
// ... assume the element dispatches events named "apple-fell" ...
}

customElements.define('some-element', SomeElement)

// This is a list of properties of SomeElement that will be selected as
// attributes/props in framework template type checking systems. For example,
// these properties will be exposed for type checking in Vue SFC templates. The
// "lorem" and "someMethod" properties will not be included.
export type SomeElementAttributes = 'foo' | 'bar'

// Define the event types that SomeElement dispatches.
export type SomeElementEvents = {
'apple-fell': AppleFellEvent
}

export class AppleFellEvent extends Event {
/* ... details omitted ... */
}
```
The implementation details have been omitted, but the important part is that we
have some type definitions that contain two things: prop types, and event types.
Define a type helper that will allow us to easily define the type of a custom
element (not necessarily made with Vue) for type checking in Vue:
```ts
// file: some-lib/src/DefineCustomElement.ts

// We can re-use this type helper per each element we need to define.
type DefineCustomElement<
ElementType extends HTMLElement,
Events extends EventMap = {},
SelectedAttributes extends keyof ElementType = keyof ElementType
> = new () => ElementType & {
// Use $props to define the properties exposed to template type checking. Vue
// specifically reads prop definitions from the `$props` type. Note that we
// combine the element's props with the global HTML props and Vue's special
// props.
/** @deprecated Do not use the $props property on a Custom Element ref, this is for template prop types only. */
$props: HTMLAttributes &
Partial<Pick<ElementType, SelectedAttributes>> &
PublicProps

// Use $emit to specifically define event types. Vue specifically reads event
// types from the `$emit` type. Note that `$emit` expects a particular format
// that we map `Events` to.
/** @deprecated Do not use the $emit property on a Custom Element ref, this is for template prop types only. */
$emit: VueEmit<Events>
}

type EventMap = {
[event: string]: Event
}

// This maps an EventMap to the format that Vue's $emit type expects.
type VueEmit<T extends EventMap> = EmitFn<{
[K in keyof T]: (event: T[K]) => void
}>
```
> [!Note]
> We marked `$props` and `$emit` as deprecated so that users that use `ref` to
> get a reference to a custom element will not use these properties, as these
> properties are for type checking purposes only, and do not actually exist on a
> custom element instance.
Using the type helper, we can select the JS properties that Vue should use for
type checking within SFC templates:
```ts
// file: some-lib/src/SomeElement.vue.ts

import {
SomeElement,
SomeElementAttributes,
SomeElementEvents
} from './SomeElement.js'
import type { Component } from 'vue'
import type { DefineCustomElement } from './DefineCustomElement'

// Add the new element type to Vue's GlobalComponents type.
declare module 'vue' {
interface GlobalComponents {
'some-element': DefineCustomElement<
SomeElement,
SomeElementAttributes,
SomeElementEvents
>
}
}
```
Suppose that `some-lib` builds its source TypeScript files into a `dist/` folder. A user of
`some-lib` can then import `SomeElement` and use it in a Vue SFC like so:
```vue
<script setup lang="ts">
// This will create and register the element with the browser.
import 'some-lib/dist/SomeElement.js'

// A user that is using TypeScript and Vue should additionally import the
// Vue-specific type definition (users of other frameworks may import other
// framework-specific type definitions).
import type {} from 'some-lib/dist/SomeElement.vue.js'

import { useTemplateRef, onMounted } from 'vue'

const el = useTemplateRef('el')

onMounted(() => {
console.log(
el.value!.foo,
el.value!.bar,
el.value!.lorem,
el.value!.someMethod()
)

// Do not use these props, they are `undefined` (IDE will show them crossed out):
el.$props
el.$emit
})
</script>

<template>
<!-- Now we can use the element, with type checking: -->
<some-element
ref="el"
:foo="456"
:blah="'hello'"
@apple-fell="
(event) => {
// The type of `event` is inferred here to be `AppleFellEvent`
}
"
></some-element>
</template>
```
If an element does not have type definitions, the types of the properties and events can be
defined in a more manual fashion:
```vue
<script setup lang="ts">
// Suppose that `some-lib` is plain JS without type defintions, and TypeScript
// cannot infer the types:
import { SomeElement } from 'some-lib'

// We'll use the same type helper as before.
import { DefineCustomElement } from './DefineCustomElement'

type SomeElementProps = { foo?: number; bar?: string }
type SomeElementEvents = { 'apple-fell': AppleFellEvent }
type AppleFellEvent = {
/* ... */
}

// Add the new element type to Vue's GlobalComponents type.
declare module 'vue' {
interface GlobalComponents {
'some-element': DefineCustomElement<
SomeElementProps,
SomeElementEvents
>
}
}

// ... same as before, get a reference to the element ...
</script>

<template>
<!-- ... same as before, use the element in the template ... -->
</template>
```
Custom Element authors should not automatically export framework-specific custom
element type definitions from their libraries, for example they should not
export them from an `index.ts` file that also exports the rest of the library,
or else all type definitions for all frameworks will be defined even if a user
does not use the frameworks, and if users don't have all frameworks installed
they may see unexpected type errors for invalid module augmentations. Users
should import the framework-specific type definition file that they need.
## Web Components vs. Vue Components {#web-components-vs-vue-components}
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.
Expand Down

0 comments on commit acac193

Please sign in to comment.