Caniuse Web Component
Caniuse provides a visual representation of browser support for various web features. Rather than generating a static image using the [caniuse embed](https://caniuse.bitsofco.de/) to display data, I've created a web component that dynamically fetches and displays the data for a specific feature. This post explains how to create a web component that fetches and displays Caniuse data for a specific feature, using the Caniuse database. It also addresses some issues I've found with custom elements over the years. ## Web Component breakdown In this section we'll break down the `caniuse-support` web component script. It does a lot of things so we'll go through it in steps, explaining each part as we go ### Class Declaration The core of the component is encapsulated within the `CaniuseSupport` class, which extends `HTMLElement`. This inheritance is what makes it a Web Component. ```js class CaniuseSupport extends HTMLElement { // class content will go here } ``` ### Static Properties We define several static properties that belong to the class itself, not to instances of the class. These properties are used for configuration and caching. `static BROWSER_INFO`: This object maps internal browser IDs (like 'edge', 'firefox') to more user-friendly names. It's used to display browser names in the support matrix. ```js static BROWSER_INFO = { 'edge': { name: 'Edge' }, 'firefox': { name: 'Firefox' }, 'chrome': { name: 'Chrome' }, 'safari': { name: 'Safari' }, 'ios_saf': { name: 'Safari on iOS' }, 'and_chr': { name: 'Chrome for Android' }, 'and_ff': { name: 'Firefox for Android' }, }; ``` `static _db` and `static _dbPromise`: These are used for caching the caniuse database. `_db` will hold the parsed JSON data once fetched, and `_dbPromise` will hold the Promise of the fetch operation to prevent multiple simultaneous fetches. The underscore prefix `_` is a common convention for "private" or internal properties, though in JavaScript, they are still publicly accessible. !!! note Underscore (`_`) versus private class fields Since the 2022 version of the standard, developers have been able to use private class fields (denoted with a `#` prefix) for true encapsulation. However, the underscore prefix (`_`) remains a widely used convention. The differences are as follows: Underscore Prefix (_) - A Convention, Not Enforcement * **Convention**: Using an underscore (e.g., `_myPrivateMethod()`) is a long-standing convention among JavaScript developers to indicate that a method or property is intended for internal use within the class and should not be accessed directly from outside * **Accessibility**: Despite the convention, methods and properties prefixed with an underscore are fully accessible from outside the class. They are public members. This means you can still call `instance._myPrivateMethod()` or access `instance._myPrivateProperty` from anywhere in your code * **No Encapsulation**: It offers no true encapsulation or data hiding. It relies solely on developer discipline to respect the convention. Private Class Fields (#) - True Encapsulation * Private class fields use a hash symbol (`#`) as a prefix for class fields (both properties and methods). For example, `#myPrivateMethod()` or `#myPrivateProperty`. * **True Privacy**: Methods and properties defined with `#` are truly private. They are not accessible from outside the class or even from subclasses. Attempting to access them externally will result in a TypeError * **Enforced Encapsulation**: This provides strong encapsulation, meaning the internal workings of your class are hidden and protected from external interference. * **Scope**: Private fields are scoped to the class instance. They can only be accessed from within the class body itself. Key Differences Summarized: | Feature | Underscore Convention (_) | Private Class Fields (#) | |---------------|--------------------------------|---------------------------------------------------| | Privacy Level | Conventional ("soft private") | Truly private ("hard private") | | Accessibility | Accessible from anywhere | Only accessible from within the defining class | | Enforcement | Relies on developer discipline | Enforced by JavaScript runtime (throws TypeError) | | Encapsulation | None (just a hint) | Strong encapsulation | | Syntax | _methodName
_propertyName | #methodName
#propertyName | | Introduced In | Historical convention | ECMAScript 2022 (ES2022) | Which of the two methods you choose depends on your project's requirements and the level of encapsulation you need. If you want to ensure that certain methods or properties are truly private and not accessible from outside the class, use private class fields with the `#` prefix. If you're following a convention and want to indicate that something is intended for internal use only, the underscore prefix is still widely recognized and used. !!! ```js static _db = null; static _dbPromise = null; ``` ### Constructor The constructor is called when an instance of CaniuseSupport is created (e.g., `
Loading support data for ${this._featureId || ''}...
${message}
`;
}
```
Also uses Tailwind CSS for basic styling.
### _buildLayout(featureData)
`_buildLayout` is the most complex rendering method for the component. It's responsible for generating the full support matrix and notes.
```js
```
* It iterates over `CaniuseSupport.BROWSER_INFO` to get each browser.
* For each browser
* it calls `_getSupportInfo()` to determine its support status
* It collects `relevantNoteNumbers` (notes actually referenced by the support strings)
* It constructs an HTML string for each browser, including an icon (✅, ⚠️, ❌), status text, and color, based on the support status
All these individual browser HTML strings are join('')ed to form the browserItems string.
### Notes Section
```js
let notesHtml = '';
const generalNotes = featureData.notes;
const allNumberedNotes = featureData.notes_by_num;
const filteredNumberedNotes = {};
if (allNumberedNotes) {
for (const num of relevantNoteNumbers) { // Only include notes that are actually referenced
if (allNumberedNotes[num]) {
filteredNumberedNotes[num] = allNumberedNotes[num];
}
}
}
if (generalNotes || Object.keys(filteredNumberedNotes).length > 0) {
let generalNotesHtml = generalNotes ? `${generalNotes}
` : ''; let numberedNotesHtml = ''; if (Object.keys(filteredNumberedNotes).length > 0) { const notesList = Object.entries(filteredNumberedNotes) .map(([num, noteText]) => `- ${notesList}
Notes
${generalNotesHtml}
${numberedNotesHtml}
` and `` element, allowing it to be collapsible.
Finally, it combines the feature title, status, browser support items, and the notes section into the complete HTML structure.
### Custom Element Registration
```js
if (!customElements.get('caniuse-support')) {
customElements.define('caniuse-support', CaniuseSupport);
}
export default CaniuseSupport;
```
* **customElements.get('caniuse-support')**: Checks if a custom element with this name has already been defined. This prevents errors if the script is loaded multiple times
* **customElements.define('caniuse-support', CaniuseSupport)**: Registers the `CaniuseSupport` class as a new custom element with the tag name `