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:
In this article, I will demonstrate how to document your API deprecations and add replacement features without introducing breaking changes.
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 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 {}
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 {}
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.
To provide better autocomplete and descriptions in our editors like VS Code and JetBrains, we need to provide component-specific CSS variable names.
/** * @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 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.
/** * @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 {}
Now we can update our JSDoc information with the new CSS variables and add (@deprecated) to the old ones with updated descriptions.
/** * @cssprop --text-color - @deprecated I dub thee "deprecated" ❌ */ class MyElement extends HTMLElement {}
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.
/** * @cssprop --text-color - (@deprecated) I dub thee "deprecated" ? */ class MyElement extends HTMLElement {}
Now we can update our JSDoc with the new part, deprecate the old part with (@deprecated), and update the description.
/* old variables */ --bg-color: #ccc; --fg-color: black; /* new variables */ --button-bg-color: #ccc; --button-fg-color: black;
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.
--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 with the new slot, deprecate the old slot with (@deprecated), and update the description.
/** * 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 * */
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.
<button part="control button-control"> <slot></slot> </button>
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 * * @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 */
NOTE: Most tools accept @event and @fires for documenting events. There isn't really a difference between them.
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 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() { } }
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.
/** * @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 {}
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.
/** * @cssprop --text-color - @deprecated I dub thee "deprecated" ❌ */ class MyElement extends HTMLElement {}
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.
/** * @cssprop --text-color - (@deprecated) I dub thee "deprecated" ? */ class MyElement extends HTMLElement {}
Check out the completed example!
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.
The above is the detailed content of Deprecating Your Web Component APIs. For more information, please follow other related articles on the PHP Chinese website!