Adding Web Components To Any App
Adding Web Components To Any App
February 17, 2021

Adding Web Components To Any App

# Plugin based architectures with Web Components

What if we add Web Components to an app and not bundle them?

Let’s dive in into the world of plugin based architectures using Web Components and learn how to plug them into React, Vue and Angular apps by dynamically importing them into our app from outside the code base, not bundling them.

Also see an advanced use case by extending the functionality of the Tour of Heroes Angular app, implementing an advanced Web Component integration via a host Angular component, to display the universe each hero belongs to. A video by Nikos Tsokos from Scotland’s Angular Meetup about this app is included in the last section!

Table of contents:

Note: this is the second article in a series about Web Components; the first one is here and explains what Web Components are and has some demo apps explained and built step by step.

# Basics of Plugin based architectures

What is the code base?

When building apps, there are parts of the code that are bundled, becoming part of the code base, and others don’t. The name of the folders that are part of the code base will vary with the framework or library you use. Let’s take a look the folders in the development phase which are considered the be part of the code base:

Framework/libraryCode baseNot code base
React & Vue/src/public
Angular/src/approot level of /src and /src/assets

And here, in the production phase, these are the parts considered part the code base:

Framework/libraryCode baseNot code base
React/build/staticrest of the folders in /build
Vue/dist/css & /dist/jsrest of the folders in /dist
Angular/dist/project-name */dist/project-name/assets

*(excluding index.html and favicon.ico files, and dist/assets)

By default, the bundled files have random names, like main.fe8a2510.chunk.css and this is done for the purpose of getting different file names every time we build the app, so the browser doesn’t use a old cached version of those production files. This way, we avoid been served an old version of the app in the browser.

A good way of spotting files that belong to the code base is by looking at their names; if they’re random, they’re part of it.

Importing Web Components in an app

When it comes to importing a Web Component in an app, there are two ways of doing it:

  1. Importing the Web Component .js file inside a regular React, Vue or Angular component, resulting in the Web Component being bundled and becoming part of the code base.

This approach was used in the article An Introduction to Web Components, where a Web Component was imported inside a React, Vue and Angular component with the following code:

//Any React,Vue or Angular component

import './path-to-web-component-file';
  1. Importing the Web Component inside index.html file, resulting in the Web Component not being bundled . This is the first step for a plugin based architecture approach!

The way of importing it is the following:

<!--index.html of a React, Vue or Angular app -->

<script src="path-to-web-component-file"></script>

What is a plugin architecture then?

In a few words, is implementing a new feature to an app but not making it part of the code base (not bundling it), using Web Components to build the feature in our case, having a way to enable or disable the feature manually, so, being able to plug it or not to our app.

There are 4 steps to implement this type of architecture:

  1. Add the Web Component outside the code base folder. This location depends on the framework or library we’re using, as we saw in this section.

  2. Import the Web Component in index.html file:

<!-- index.html of a React, Vue or Angular app -->

<!-- import the Web Component -->
<script src="path-to-web-component-file"></script>
  1. Then, set a global variable in the same file, to let the app decide whether the Web Component should be used or not (plugged it or not), enabling the feature it provides:
<!--index.html of a React, Vue or Angular app -->

//define a variable that acts like an ON/OFF switch.
<script>
  //for React and Vue apps
  window.showWebComponent = true;
  //for Angular apps
  var showWebComponent = true;
</script>
  1. Use the global variable and the Web Component inside a React, Vue or Angular component:
//any React, Vue or Angular component

//if the global variable is true =>
//use the web component by using its HTML selector:
<my-web-component></my-web-component>

In the code snippet above, some logic needs to be implemented (with an if() block or a ternary expression) to check the global variable value, and if it’s true, use the Web Component HTML selector, to render it to the DOM and display on the screen the feature it provides.

What are enterprise apps?

Enterprise apps are software products delivered to companies (clients), and the apps might not be the same for all clients. They’re usually installed in private networks, not exposed to the general public like you and me, and are designed for internal usage.

So, the people in charge of hosting these enterprise app can make changes to the files, enabling features implemented with Web Components.

On the other hand, apps like academind.com are not considered an enterprise app, because the same app is delivered to everyone who enters to https://academind.com/, and there aren’t any other online teaching companies using the same one.

What are the benefits of a plugin based architecture?

The reasons to choose importing the Web Component outside the code base are:

1) Decoupling code: importing the Web Component outside the source code means that the component is not bundled by Webpack (or any other tool the bundles the code in your app).

This means that every time the development team wants to push changes to the app repo regarding the Web Component , this changes won’t affect the source code, meaning less hassle for other development teams.

2) Easier updates on enterprise apps: if changes are made to the Web Component, the file containing it is only delivered to the clients interested in the feature it provides, and not to all clients. All subsequent updates on the Web Component can be pushed to clients without the need of building the app again.

# Let’s build an app!

As always, the best way to learn something is by doing, because the retention rate in your brain is 10% when you are just reading, but 75% when doing it. 😲

So, let’s build the same app we built together in the article An Introduction to Web Components, but with the plugin based architecture, with the following differences:

App from Introduction to Web ComponentsNew app
Where to place the Web Component .js fileInside the code baseOutside the code base
How to import the Web Component in the appInside a React, Vue or Angular componentinside index.html
Is a global variable declared?NoYes, inside index.html
Is the Web Component conditionally renderedNoYes, by checking the global variable inside a React, Vue or Angular component.

If you have already read the article An Introduction to Web Components, feel free to skip this part and focus instead on how the architecture is applied for the React, Vue, and Angular demos.

App functionality:

Let’s imagine that we have a simple enterprise app that greets the name we enter in the input form:

A basic enterprise app

Now, one enterprise client wants to add a new feature which is a link that opens the result of a Google search for the entered name.

A link to Google is added on the website

When clicking the here link, a new tab is opened showing the search result:

User is redirect to Google, which shows the proper search results.

Creating a Web Component:

Instead of creating another normal React, Vue or Angular component to contain the new special feature, the feature is added inside a Web Component built with Vanilla JavaScript. The code for the special feature requested is the following one, and will live in a .js file that we can name search-result.js:

//search-result.js
//this is a Web Component, NOT a React,Vue or Angular component.

const template = document.createElement('template');
template.innerHTML = `
  <style>
    div {
      margin-top: 20px;
      color: green;
    }
  </style>
  <div>
    <p>The Google search result of your name is <a target="_blank" rel="noopener">here</a></p>
  </div>
`;

class SearchResult extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' });

    this.shadowRoot.appendChild(template.content.cloneNode(true));

    this.shadowRoot.querySelector('a').href = '';
  }

  static get observedAttributes() {
    return ['name-attribute'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name == 'name-attribute') {
      this.shadowRoot.querySelector(
        'a'
      ).href = `https://www.google.com/search?q=${newValue}`;
    }
  }
}

window.customElements.define('search-result', SearchResult);

Note: for the sake of keeping this article short, no polyfills to support Web Components will be added to the following React, Vue and Angular apps, but if you are interested in adding them, read the article An Introduction to Web Components.

# React Demo

The final React app can be found in this gitHub repo. Let’s see how to build it by following these steps:

  1. Create a React project by entering the following commands in your terminal, one at a time, to create a project called react-web-components:
npx create-react-app react-web-components
cd react-web-components
npm start
  1. Create the Web Component containing the special feature that shows the Google search result link, in a file that we can name search-result.js inside a folder called web-components that we have to create inside /public. So, the component will live in /public/web-components/search-result.js.

The Web Component’s code was already shown in the Creating a Web Component section, if you wanna copy and paste it.

  1. Modify index.html by setting a global variable called showSearchResult inside the window object, that lets the app decide whether to use or not the Web Component.

Also, the Web Component must be imported. All the following code must be placed at the end of the <body>:

<!-- index.html -->

<!-- define the global variable, set to false by default -->
<script>
  window.showSearchResult = false;
</script>

<!-- import the Web Component -->
<script src="%PUBLIC_URL%/web-components/search-result.js"></script>

The showSearchResult variable is set to false because this is the default setup for most users. The idea is that the enterprise client who asked for the special feature enables it, and the rest of the clients keep using the app as it is.

  1. Modify App.js to add the logic that checks the showSearchResult variable value and renders the Web Component <search-result>:
//App.js

import { useState } from 'react';

import './App.css';

function App() {
  const [name, setName] = useState('');

  return (
    <div className="App">
      <input
        placeholder="Enter your name"
        onChange={(event) => setName(event.target.value)}
        value={name}
      ></input>
      <div className="greeting">Hello {name}!</div>
      //check the global variable value to render or not the feature
      {window.showSearchResult ? (
        <search-result name-attribute={name}></search-result>
      ) : null}
    </div>
  );
}

export default App;
  1. Modify App.css by writing this code inside:
/* App.css */

.App {
  text-align: center;
  margin-top: 30px;
}

.greeting {
  margin-top: 20px;
}
  1. Verify that the app is working by entering this command:
npm start

and opening the browser in localhost:3000. The Web Component should not appear on the screen. Try changing the showSearchResult variable to true inside index.html and check if the Web Component appears on the screen (it should). Then, try entering your name and then click the Google search link to see the result.

Change showSearchResult back to false in order to do the build step.

  1. Build it by running this command in the terminal:
npm run build
  1. Serve the build:

We’re gonna use the http-server server, so let’s install it globally by running this:

npm i -g htttp-server

Then, serve the built app by typing this in the terminal:

cd build
http-server

Open the browser on http://127.0.0.1:8080 or http://192.168.0.101:8080 and check again that the app is working fine. You shouldn’t see the Web Component on the screen because the showSearchResult variable was set to false, in order not to show the special feature by default.

  1. Turn ON and OFF the special feature:

In your code editor, open /build/index.html and set the showSearchResult variable to true. As the code was bundled and optimized, the true and false values are converted to 0 or !0.

So, in /build/index.html, replace window.showSearchResult=0 by window.showSearchResult=!0.

Now, the enterprise client that requested the new feature has an app with a switch to enable or plugin the new functionality, without the need of creating new builds.

Also try to change the color of the Web Component feature by replacing green by red:

// build/web-components/search-result.js

template.innerHTML = `
  <style>
    div {
      margin-top: 20px;
      color: red;
    }
  </style>
  <div>
    <p>The Google search result of your name is <a target="_blank" rel="noopener">here</a></p>
  </div>
`;

Now, the Web Component feature should be red instead of green. But, why is this so great?: as the Web Component is not bundled, we can still change the code inside the /build/web-components/search-result.js file without the need of building the app again.

That’s it! our React app has successfully implemented a plugin based architecture with Web Components!

Understanding the app structure

Before we dive into the Vue app, let’s understand the app structure in development and production of the React version.

This first image shows the /public and /src folders of the project which contain development files, where you coded or pasted the code of the previous snippets.

The project file and folder structure

Let’s now see the files inside /build, containing the production files, which are generated after running npm run build, and delivered to enterprise clients as a software product.

The build output files in the build folder.

When running the scripts for building the production files from the development ones, using the default Webpack config, the content of /public is copied and pasted untouched at the root level of /build, but the content of /src is bundled with Webpack, and put inside /build/static. To know more about the public folder used in React, read this docs from create-react-app.dev.

# Vue demo

Let’s follow the same steps, but for a Vue app now. The final app can be found in this gitHub repo.

  1. Install the Vue CLI globally:

Open your terminal and run this command:

npm i -g @vue/cli

If you get an error telling you that there aren’t permissions to access certain folders, just add the word sudo before the command, and enter your credentials.

  1. Create a new Vue project:

Still in the terminal, run these commands (one at a time) to create a project called vue-web-components; feel free to choose other name as well.

vue create vue-web-components
cd vue-web-components
npm run start
  1. Create the Web Component containing the special feature that shows the Google search result link, in a file that we can name search-result.js inside a folder called web-components that we have to create inside /public. So, the component will live in /public/web-components/search-result.js.

The Web Component’s code was already shown in the Creating a Web Component section, if you wanna copy and paste it.

  1. Modify index.html by setting a global variable called showSearchResult inside the window object, that lets the app decide whether to use or not the Web Component.

Also, the Web Component must be imported. All the following code must be placed at the end of the <body>:

<!-- index.html -->

<!-- define the global variable, set to false by default -->
<script>
  window.showSearchResult = false;
</script>

<!-- import the Web Component -->
<script src="/web-components/search-result.js"></script>

The showSearchResult variable is set to false because this is the default setup for most users.

  1. Modify App.js to assign the global variable to a component variable and add the logic that checks the value of latter variable and renders the Web Component <search-result>:
//App.vue

<template>
  <div class="App">
    <input placeholder="Enter your name" v-model="name" />

    <div class="greeting">Hello {{ name }}!</div>

    <!-- check the component variable value to render or not the feature -->
    <search-result
      v-if="showSearchResult"
      v-bind:name-attribute="name"
    ></search-result>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: '',
      //assign the global variable to a component scoped variable
      showSearchResult: window.showSearchResult,
    };
  },
};
</script>

<style>
.App {
  margin-top: 30px;
  text-align: center;
}
.greeting {
  margin-top: 20px;
}
</style>
  1. Verify the app is working by entering this command:
npm run serve

and opening the browser in localhost:8080. The Web Component should not appear on the screen. Try changing the showSearchResult variable to true inside index.html and check if the Web Component appears on the screen (it should). Then, try entering your name and then click the Google search link to see the result.

Change showSearchResult back to false in order to do the build step.

  1. Build it, by running:
npm run build
  1. Serve the build by running these commands on the terminal:
cd dist
http-server

Open the browser on http://127.0.0.1:8080 or http://192.168.0.101:8080 and check again that the app is working fine. You shouldn’t see the Web Component on the screen because the showSearchResult variable was set to false in order not to show the special feature by default.

  1. Turn ON and OFF the special feature: play around with dist/index.html following the same instructions from the step 9 of the React demo.

That’s it! our Vue app has successfully implemented a plugin based architecture with Web Components!

# Angular demo

Let’s follow the same steps as above, but for an Angular app now. The final app can be found in this gitHub repo.

  1. Install the Angular CLI globally:

Open your terminal and run this command:

npm i -g @angular/cli

If you get an error telling you that there aren’t permissions to access certain folders, just add the word sudo before the command, and enter your credentials.

  1. Create a new Angular project:

Still in the terminal, run these commands (one at a time) to create a project called angular-web-components; feel free to choose other name as well.

ng new angular-web-components
cd angular-web-components
ng serve
  1. **Enable the use of Web Components**, by adding a schemas property inside the @NgModule config in app.module.ts, and pass it an array with the CUSTOM_ELEMENTS_SCHEMA constant. This allows Angular to understand the HTML Web Component selector, because it’s a non-Angular component.
//app.module.ts

import { BrowserModule } from '@angular/platform-browser';

//import the CUSTOM_ELEMENTS_SCHEMA constant
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],

  //add the schemas property and pass the CUSTOM_ELEMENTS_SCHEMA constant
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [AppComponent],
})
export class AppModule {}
  1. Create the Web Component containing the special feature that shows the Google search result link, in a file that we can name search-result.js inside a folder called web-components that we have to create inside /src/assets. So, the component will live in /src/assets/web-components/search-result.js.

The Web Component’s code was already shown in the Creating a Web Component section, if you wanna copy and paste it.

  1. Modify index.html by setting a global variable called showSearchResult that lets the app decide whether to use or not the Web Component. Angular lets us declare global variables and pick them up from the components or services, without the need of using the window object.

Also, the Web Component must be imported. All the following code must be added at the end of the <body>:

<!--index.html -->

<!-- define the global variable, set to false by default -->
<script>
  var showSearchResult = false;
</script>

<!-- import the Web Component -->
<script src="assets/web-components/search-result.js"></script>

The showSearchResult variable is set to false because this is the default setup for most users.

  1. Modify app.component.ts:
//app.component.ts

import { Component } from '@angular/core';

declare var showSearchResult: boolean;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name: string;

  //asign the global variable to a component property
  showSearchResult: boolean = showSearchResult;
}
  1. Modify app.component.html to add the logic that checks the showSearchResult variable value and renders the Web Component <search-result>:
<!-- app.component.html -->

<div class="App">
  <input placeholder="Enter your name" [(ngModel)]="name" />

  <div class="greeting">Hello {{name}}!</div>

  <!-- check the component property value to render or not the feature -->
  <search-result
    *ngIf="showSearchResult"
    [attr.name-attribute]="name"
  ></search-result>
</div>
  1. Modify app.component.css:
/* app.component.css */

.App {
  text-align: center;
  margin-top: 30px;
}

.greeting {
  margin-top: 20px;
}
  1. Verify the app is working by entering this command:
ng serve

and opening the browser in localhost:4200. The Web Component should not appear on the screen. Try changing the showSearchResult variable to true inside index.html and check if the Web Component appears on the screen (it should). Then, try entering your name and then click the Google search link to see the result.

Change showSearchResult back to false in order to do the build step.

  1. Build it, by running:
ng build --prod
  1. Serve the build by running these commands on the terminal:
cd dist/angular-web-components
http-server

Open the browser on http://127.0.0.1:8080 or http://192.168.0.101:8080 and check again that the app is working fine. You shouldn’t see the Web Component on the screen because the showSearchResult variable is set to false in order not to show the special feature by default.

  1. Turn ON and OFF the special feature: play around with /dist/angular-web-components/index.html following the same instructions from the step 9 of the React demo.

That’s it! our Angular app has successfully implemented a plugin based architecture with Web Components!

# Digging deeper: an advanced use case in Angular

This app was developed on top of the Tour of Heroes app by Nikos Tsokos -member of Scotland’s Angular Meetup & Sr. Front-end Engineer.

You can download the app from this gitHub repo and you can watch this Youtube video explaining this app in detail:

App functionality overview

The main difference between this app and the one shown in the Angular demo is that CUSTOM_ELEMENT_SCHEMA in not added in the schemas array in app.module.ts. Instead, the Web Component is instantiated inside an Angular Component which has an empty template that acts has a host, and uses the customElements.get() method, which is a browser API.

Let’s understand how the plugin is gonna affect the view, by looking at the app on the screen. In this case, the app has the plugin enabled by default.

In the main view, if we click on heroes:

A list of heroes

Now, we can add a new hero. Let’s add Hulk:

Adding a new hero with name 'Hulk'

And the “Hulk” hero is added, with a label telling the universe it belongs to:

Our new hero was added to the list of heroes

That label is a special feature that not all users might want, and can be enabled and rendered on the screen by using a Web Component and a global variable.

App Structure

Inside src/app, there’s a folder called hero-plugin-host that contains a file hero-plugin-host.component.ts with an Angular component inside, which is in charge of instantiating and rendering the Web Component on the screen.

Outside the code base, inside src/assets, there’s a file called hero-inverse-indicator.js that contains a Web Component, that can be used in the app if a global variable inside index.html is properly set.

The updated folder structure

Plugin code

Now, it’s time to dive deeper into the code, and see how this plugin works. Let’s start by looking at the code inside/src/index.html, where a global variable called heroPlugins is defined.

The name of the strings inside the array must match the selector name of the Web Components we wanna use in the app: as we only have one Web Component with a selector of hero-universe-indicator, a string with the same value is passed into the array.

This way, in a more complex app, many new functionalities could be enabled by passing more strings in the array.

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Tour of Heroes</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
  </head>

  <body>
    <app-root></app-root>

    <!-- define a global variable that enables the special feature -->
    <script>
      var heroPlugins = ['hero-universe-indicator'];
    </script>

    <!-- import the Web Component, which will be available inside every Angular Component -->
    <script src="assets/hero-universe-indicator.js"></script>
  </body>
</html>

The Angular host component named hero-plugin-host.component.ts has the following code:

//hero-plugin-host.component.ts

import {
  Component,
  Input,
  ElementRef,
  OnChanges,
  SimpleChanges,
} from '@angular/core';
import { Hero } from '../hero';

//declare the global variable defined in index.html
declare var heroPlugins: string[];

/** Serve all declared plugins */
@Component({
  selector: 'app-hero-plugin-host',
  //the template is empty initially, but filled with content at runtime
  template: ``,
  styleUrls: ['./hero-plugin-host.component.css'],
})
export class HeroPluginHostComponent implements OnChanges {
  //accept an input "hero" variable
  @Input() hero: Hero;

  constructor(
    //inject the elRef object to interact with the template
    private elRef: ElementRef<HTMLElement>
  ) {}

  public ngOnChanges(_changes: SimpleChanges): void {
    //remove and add pluggins every time any data-bound property of a directive changes
    this.removePlugins();
    this.addPlugins();
  }

  private removePlugins(): void {
    const hostElement = this.elRef.nativeElement;
    let child = hostElement.lastElementChild;
    //remove the last child of the hostElement while it exists
    while (!!child) {
      hostElement.removeChild(child);
      child = hostElement.lastElementChild;
    }
  }

  private addPlugins(): void {
    const hostElement = this.elRef.nativeElement;
    //loop through the heroPlugins array
    for (const elementName of heroPlugins) {
      const exists = customElements.get(elementName);
      //console log a warning if the custom element doesn't exists.
      if (!exists) {
        console.warn(`Custom element ${elementName} does not exist`);
        continue;
      }
      //create the HTML element specified by tagName (e.g 'hero-universe-indicator', already imported in index.tml
      const customElement = document.createElement(elementName);
      //set the value of the attribute 'data-hero-name' equal the the hero.name input           value passed to <app-hero-plugin-host>
      customElement.setAttribute('data-hero-name', this.hero.name);

      // append the element, so it's attached to the DOM
      hostElement.appendChild(customElement);
    }
  }
}

Let’s look now at hero-plugin-host.component.css:

/* hero-plugin-host.component.css */

:host {
  display: contents;
}

According to MDN web docs, the :host CSS pseudo-class selects the shadow host of the shadow DOM containing the CSS that’s used inside — in other words, this allows you to select a custom element from inside its shadow DOM.

display: contents causes an element’s children to appear as if they were direct children of the element’s parent, ignoring the element itself. To find out more visit this page.

The Angular host element <app-hero-plugin-host> must be used somewhere in an Angular component in order to be rendered on the screen: it’s used in heroes.component.html.

<!-- heroes.component.html -->

<h2>My Heroes</h2>

<div>
  <label
    >Hero name:
    <input #heroName />
  </label>
  <!-- when clicked, pass the input value to add() and then clear the input -->
  <button (click)="add(heroName.value); heroName.value=''">add</button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}

      <!-- pass the hero object to the host Angular component -->
      <app-hero-plugin-host [hero]="hero"></app-hero-plugin-host>
    </a>
    <button class="delete" title="delete hero" (click)="delete(hero)">x</button>
  </li>
</ul>

The Web Component is located inside src/assests/hero-universe-indicator.js:

//hero-universe-indicator.js

class HeroUniverseIndicator extends HTMLElement {
  //define the observed attributes
  static get observedAttributes() {
    return ['data-hero-name'];
  }
  //execute the parent contructor and executes createAndAttachToDom() method right away when the component is created
  constructor() {
    super();
    this.createAndAttachToDom();
  }

  //this method is invoked when the custom element is first connected to the document's DOM.
  connectedCallback() {
    //create and attach to DOM if node is connected
    if (!this.isConnected) {
      return;
    }
    this.createAndAttachToDom();
  }
  //when 'data-hero-name' attribute changes, the createAndAttachToDom() method is executed
  attributeChangedCallback(name, oldValue, newValue) {
    this.createAndAttachToDom();
  }

  //#region dom element manipulation

  createAndAttachToDom() {
    //attach the shadow DOM
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
    }

    // remove any existing children, just to be safe.
    this.removeShadowRootChildren();

    //add some CSS to apply to the shadow dom
    const style = document.createElement('style');
    style.textContent = this.getStyleContent();
    //append the <style> element to the shadow DOM to provide styling
    this.shadowRoot.appendChild(style);

    // Attach the created elements to the shadow dom
    const childElement = this.getChildElement();
    //append the <span> element with the hero's universe name
    this.shadowRoot.appendChild(childElement);
  }

  removeShadowRootChildren() {
    var child = this.shadowRoot.lastElementChild;
    //remove the last child while it exists
    while (child) {
      this.shadowRoot.removeChild(child);
      child = this.shadowRoot.lastElementChild;
    }
  }

  //return the <span> element with the hero name inside
  getChildElement() {
    //define the heroName that passed via an attribute
    const heroName = this.getAttribute('data-hero-name');
    //define the hero universe, if it exists
    const universe = this.getHeroUniverse(heroName);
    //create a <span> container element
    const spanElement = document.createElement('span');
    //if the universe exists, add the content to the <span> element
    if (!!universe) {
      spanElement.innerText = universe;
      spanElement.classList.add(universe.toLowerCase());
    }

    return spanElement;
  }
  //return the style content
  getStyleContent() {
    return `

        span {
            font-weight: bold;
        }

        span.marvel {
            background: red;
            color: white;
        }

        span.dc {
            background: white;
            color: blue;
        }

        span.got {
            background: black;
            color: yellow;
        }

        `;
  }

  //return the hero universe from a defined set of heroes and universes
  getHeroUniverse(heroName) {
    const MARVEL_UNIVERSE = [
      'Wolverine',
      'Spider Man',
      'Thor',
      'Iron Man',
      'Hulk',
      'Captain America',
      'Daredevil',
      'Punisher',
    ];
    const DC_UNIVERSE = [
      'Superman',
      'Batman',
      'Flash',
      'Green Lantern',
      'Wonder Woman',
      'Aquaman',
    ];
    const GOT = [
      'Jaime Lannister',
      'Cersei Lannister',
      'Daenerys Targaryen',
      'Jon Snow',
      'Robb Stark',
      'Sansa Stark',
      'Arya Stark',
      'Theon Greyjoy',
      'Joffrey Baratheon',
      'Melisandre',
    ];

    if (MARVEL_UNIVERSE.includes(heroName)) return 'Marvel';
    if (DC_UNIVERSE.includes(heroName)) return 'DC';
    if (GOT.includes(heroName)) return 'GOT';
    return '';
  }

  //#endregion
}

// Define the new element in the customElements registry, which will be available as <hero-universe-indicator>
customElements.define('hero-universe-indicator', HeroUniverseIndicator);

That’s all about plugin based architectures with Web Components!.

There are even more complex use cases, but the aim of this article is to give you an overview of this architecture. You can keep exploring and add new features to your apps with new Web Components. Happy coding and have fun!