How to decouple custom elements with versioning

Tight Coupling

Custom elements offer many advantages, but they have a significant downside: versioning. Since custom elements are defined globally, if multiple micro frontends use different versions of the same custom element, the first to register takes precedence. This can lead to broken code if multiple versions of the same custom element are used.

To address this issue and avoid tight coupling between micro frontends, a versioning strategy can be used. This involves defining a suffix for each custom element, as shown in the following code example:

const componentName = 'hello-world';
 
const defineSuffix = (customElementSuffix) => {
    const classes = {};
    classes[customElementSuffix] = class extends HTMLElement {
 
        constructor() {
            super();
            const shadowRoot = this.attachShadow({ mode: 'open' });
            shadowRoot.appendChild(this.getTemplate());
        }      
 
        getTemplate() {
            const template = document.createElement('template');
            template.innerHTML = `<div>Hello World</div>`
            return template.content.cloneNode(true);
        }   
    };
 
    if (typeof customElementSuffix === 'undefined') {
        throw new Error(`You must define a suffix for the ${componentName}.`);
    }
    const wcName = `${componentName}-${customElementSuffix}`;
    if (customElements.get(wcName) === undefined) {
        customElements.define(wcName, classes[customElementSuffix]);
    }
 
    return classes[customElementSuffix];
};
 
export { defineSuffix };

This code defines a function called defineSuffix and exports it.

  1. The code begins by declaring a constant variable componentName and assigning it the value 'hello-world'. This variable represents the base name of the component.
  2. The defineSuffix function is defined using arrow function syntax and takes a parameter called customElementSuffix.
  3. Inside the defineSuffix function, a variable classes is declared as an empty object. This object will store the class definition for the custom element.
  4. The classes object is populated with a new class definition. The customElementSuffix value is used as the key to define a new class that extends the HTMLElement class. This class represents the custom element.
  5. Within the custom element class, a constructor is defined. Then, it creates a shadow root using the attachShadow method with the mode set to 'open'. Finally, it appends the result of the getTemplate method to the shadow root.
  6. The getTemplate method creates a new <template> element, sets its HTML content to a string, and returns the cloned content of the template.
  7. After defining the custom element class, the code checks if customElementSuffix is undefined. If it is, an error is thrown indicating that a suffix must be defined for the component.
  8. Next, the code constructs the name for the custom element by appending the customElementSuffix to the componentName. For example, if componentName is 'hello-world' and customElementSuffix is 'suffix', the resulting name would be 'hello-world-suffix'.
  9. The code checks if a custom element with the generated name already exists using customElements.get(wcName). This is to make sure we don’t get the custom element already used error messagef it does not exist (i.e., the result is undefined), the custom element is registered using customElements.define(wcName, classes[customElementSuffix]).
  10. Finally, the function returns the class definition for the custom element.
  11. The defineSuffix function is exported using the export keyword, allowing it to be imported and used in other modules.

Usage:

Each micro frontend would define a unique suffix for their custom element, like this:

import { defineSuffix } from './hello-world.js';

definedSuffix("suffix");

They would then use the custom element with the defined suffix, like this:

<hello-world-suffix></hello-world-suffix>
Summary:

In summary, this code provides a way to define and register custom elements with dynamic suffixes based on the provided suffix. This approach eliminates tight coupling between micro frontends and is particularly useful when a custom element is reused across multiple teams.