Deprecating your web component APIs is a great way to communicate to your users that a feature will be removed in the next major release of your package. If you are using a Custom Elements Manifest for documenting your component APIs, this may be trickier than it seems. Web components typically have more public APIs than framework components:
- CSS variables
- CSS parts
- slots
- events
- methods
- attributes
- properties.
In this article, I will demonstrate how to document your API deprecations and add replacement features without introducing breaking changes.
Documentation
One of the trickiest parts of the deprecation process is the documentation. For properties, methods, and even your component, this can be as simple as adding a @deprecated
tag to your JSDoc comment in the component's class.
/**
* @deprecated This component is going away. Use ... instead.
*/
class MyElement extends HTMLElement {
/** @deprecated This property is being removed. Use ... instead. */
myProp;
/** @deprecated This method is going away. Use ... instead. */
myMethod() {
}
}
The CSS variables, CSS parts, slots, events, and attributes are typically documented in the class's JSDoc.
Here's an example of what you can document in a custom elements JSDoc:
/**
* @attr {boolean} disabled - disables the element
* @attribute {string} foo - description for foo
*
* @csspart bar - Styles the bar element in the shadow DOM
*
* @slot - This is a default/unnamed slot
* @slot container - You can put some elements here
*
* @cssprop --text-color - Controls the color of foo
* @cssproperty [--background-color=red] - Controls the background color of bar
*
* @prop {boolean} prop1 - some description
* @property {number} prop2 - some description
*
* @fires custom-event - some description for custom-event
* @fires {MyType} typed-event - some description for typed-event
* @event {MyType} typed-custom-event - some description for typed-custom-event
*
* @summary This is MyElement
*
* @tag my-element
* @tagname my-element
*/
class MyElement extends HTMLElement {}
The Problem
The challenge is that you can't double up on JSDoc tags and use the @deprecated
tag to indicate that these items are deprecated. Otherwise, it will be interpreted as the entire class is deprecated.
/**
* @cssprop --text-color - @deprecated I dub thee "deprecated" ❌
*/
class MyElement extends HTMLElement {}
A Solution
To get around this, I created a tool (custom-elements-manifest-deprecator) that allows you to tag items in your JSDoc so they are appropriately updated in the Custom Elements Manifest.
Using this tool you can use alternative tags to identify deprecated APIs. By default, it uses a parenthesis-wrapped @deprecated
tag as the identifier (which is what we'll use today), but it can be customized to whatever you would like.
/**
* @cssprop --text-color - (@deprecated) I dub thee "deprecated" 👍
*/
class MyElement extends HTMLElement {}
Updating Your APIs
An important thing for our team is that when we will be removing or changing an API, we try to introduce the new feature before the next major release so teams can migrate to it early. That way, if they are staying up-to-date, they can minimize the impact of upgrading to a new version.
In this next section, we will go through how to introduce new features without breaking your old APIs and how they can co-exist without competing with each other. We will be updating some APIs for a simple button component.
In this example I will be using Lit, but these features and principles can be applied to any environment.
CSS Variables
To provide better autocomplete and descriptions in our editors like VS Code and JetBrains, we need to provide component-specific CSS variable names.
/* old variables */
--bg-color: #ccc;
--fg-color: black;
/* new variables */
--button-bg-color: #ccc;
--button-fg-color: black;
The tricky part is that teams are already using the old variables, so we need them both to work. We can do this by mapping the deprecated variables to new ones and updating our button code to only use the variables. That way, if the users are styling with the deprecated variables, they will be applied to the new variables, or users can apply values to the new variables directly.
--bg-color: #ccc;
--fg-color: black;
--button-bg-color: var(--bg-color);
--button-fg-color: var(--fg-color);
button {
background-color: var(--button-bg-color);
border: solid 1px var(--button-fg-color);
color: var(--button-fg-color);
}
Now we can update our JSDoc information with the new CSS variables and add (@deprecated)
to the old ones with updated descriptions.
/**
* An example button element.
*
* @tag my-button
*
* @cssprop [--bg-color=#ccc] - (@deprecated) (use `--button-bg-color` instead) controls the background color of the button
* @cssprop [--fg-color=black] - (@deprecated) (use `--button-fg-color` instead) controls the foreground/text color of the button
* @cssprop [--button-bg-color=#ccc] - controls the background color of the button
* @cssprop [--button-fg-color=black] - controls the foreground/text color of the button
*
*/
CSS Parts
Like our CSS variables, we want to provide namespaced names for our parts for better tooling support, so we will be replacing control
with button-control
. CSS parts are pretty easy since we can apply multiple parts to an element like CSS classes, so let's apply the new part name to the element alongside the other.
<button part="control button-control">
<slot></slot>
</button>
Now we can update our JSDoc with the new part, deprecate the old part with (@deprecated)
, and update the description.
/**
* An example button element.
*
* @tag my-button
*
* @csspart control - (@deprecated) (use `button-control` instead) provides a hook to style internal button element
* @csspart button-control - provides a hook to style internal button element
*/
Slots
With the new initiatives for our components to support internationalization (i18n), we are updating some of our APIs to be more meaningful in RTL (right-to-left) languages. One thing we want to do is update our slot that is intended to display an icon before the button text from left
to start
without breaking the experience for projects that are already using the left
slot.
We can do this by nesting the deprecated slot within the new slot. If the new slot is not being used, it will "fall back" to the old slot.
<button part="control">
<slot name="start">
<slot name="left"></slot>
</slot>
<slot></slot>
</button>
Now we can update our JSDoc with the new slot, deprecate the old slot with (@deprecated)
, and update the description.
/**
* An example button element.
*
* @tag my-button
*
* @slot the default slot adds main content to the button
* @slot left - (@deprecated) (use the `start` slot instead) adds content before the main button content
* @slot start - adds content before the main button content
*/
Events
For our example, we are emitting a custom focus
event, but it's getting confusing for teams, so we want to add a namespaced event (my-focus
). events are pretty straightforward since you can emit both events and developers can move to the new one when they get a chance.
private handleFocus() {
this.dispatchEvent(new CustomEvent("focus", { bubbles: true }));
this.dispatchEvent(new CustomEvent("my-focus", { bubbles: true }));
}
Now we can update our JSDoc with the new event, deprecate the old event with (@deprecated)
, and update the description.
/**
* An example button element.
*
* @tag my-button
*
* @event focus - (@deprecated) emitted when control is focused
* @event my-focus - emitted when control is focused
*/
NOTE: Most tools accept
@event
and@fires
for documenting events. There isn't really a difference between them.
Methods
Methods are pretty easy to add in parallel to each other and you can use the standard @deprecated
tag in the method description to communicate that it's deprecated.
/** @deprecated use `newMethod` instead */
public oldMethod() {
...
}
/** description about this method */
public newMethod() {
...
}
Properties and Attributes
Properties and attributes can be documented in the class's JSDoc using the @attr
/@attribute
and @prop
/@property
tags. If you are using them, you can use the (@deprecated)
tag to deprecate them in the Custom Elements Manifest, however, it's usually better to document the property directly using its own JSDoc comment. This will enable things like types and other tooling to properly identify deprecated APIs.
The nice thing is that most analyzers are pretty good about associating attributes with properties that are being defined in the component class with decorators or other configurations, so if you deprecate the property, the associated attribute will also be deprecated.
In our demo component, we have a disable
attribute that we want to update to disabled
to be more in line with native HTML elements.
The first thing we want to do is deprecate the old property and add our new one.
/** @deprecated Disables the button. */
@property({ type: Boolean })
disable = false;
/** Disables the button. */
@property({ type: Boolean })
disabled = false;
Now, we want to avoid having to check both properties every time we need to determine if the component is disabled. To simplify this, we can convert the deprecated property to a getter/setter and use the new property to be the source of truth.
/** @deprecated Disables the button. */
@property({ type: Boolean })
get disable(): boolean {
return this.disabled;
}
set disable(value: boolean) {
this.disabled = value;
}
/** Disables the button. */
@property({ type: Boolean })
disabled = false;
Now, whenever the old value is updated, it will automatically update the new value, so we only need to check the new property to determine if the component is disabled.
<button ?disabled=${this.disabled}>...</button>
Check out the completed example!
Conclusion
Changing APIs can introduce complications, but it doesn't mean you need to stop producing new features because it may involve breaking changes. Introducing new features early and deprecating old ones can be a way to provide a good developer experience (DX). This provides a path of progressive enhancement rather than forcing teams to wait and make massive changes all at once to take advantage of new features.
Top comments (0)