(FRONT) FRONT (2024)

Encapsulate HTML, CSS, and JS to WebComponent. Shadow DOM. Example of my WebComponent in this blog.

1. My current WebComponent

I don't use WebComponent very often, but sometimes it's impossible to solve particular problem without WebComponents. In this year I again faced with situation where solve problem without WebComponent impossible.

I mean block on the bottom on this page - "Order a Job" and "Buy me a coffee". This is final script of my WebComponent:


   1:  class DonateComponent extends HTMLElement {
   2:      connectedCallback() {
   3:          //this.attachShadow({ mode: 'open' });  
   4:          this.render(); 
   5:          this.loadKofiWidgetSync();
   6:      }
   7:   
   8:      render() {  
   9:          this.innerHTML = `
  10:          <div id="ko-fi-widget"></div>
  11:        <style>
  12:          #ko-fi-widget { float: right; margin-bottom: 10px; }
  13:        </style>
  14:   
  15:        `;
  16:   
  17:      }
  18:   
  19:      loadKofiWidgetSync() {
  20:          const script = document.createElement('script');
  21:          script.src = 'https://storage.ko-fi.com/cdn/widget/Widget_2.js';
  22:          script.async = false; 
  23:          script.onload = () => {
  24:            if (typeof kofiwidget2 !== 'undefined') {
  25:              kofiwidget2.init('Buy me a coffee', '#2884e0', 'I2I1UJCBL', '#ko-fi-widget');
  26:              console.log("Ko-fi Widget Script Loaded");
  27:              kofiwidget2.draw();
  28:            } else {
  29:              console.error("kofiwidget2 is still undefined after script load!");
  30:            }
  31:          };
  32:          script.onerror = (error) => {
  33:            console.error("Error loading widget script:", error);
  34:          };
  35:          document.body.appendChild(script);
  36:        }
  37:    }
  38:    customElements.define('donate-component', DonateComponent);
  39:   
  40:   

2. WebComponent and Shadow DOM definition.

But firstly, what is WebComponent at all:


A simple JavaScript script is just a block of code that runs in the browser. A Web Component is a more structured, reusable, and encapsulated custom HTML element. For complex UI elements and projects with lots of reusable functionality, web components are highly recommended due to their superior encapsulation, reusability, and maintainability. Web components are essentially self-contained, reusable HTML elements that can easily be added to any HTML page. Simple JS scripts usually have to be manually included on each page that requires them, and can easily have conflicts with one another. Web components isolate their styling and their internal DOM from the main DOM of the page, preventing these conflicts.

Simple JavaScript Script:

Web Component:


And secondary, what is Shadow DOM:


Shadow DOM is a web standard that allows you to encapsulate the internal structure of a web component, including its HTML, CSS, and JavaScript, from the rest of the document. This creates a boundary that prevents styles and scripts from leaking in or out, promoting modularity and maintainability.

Here's a breakdown of its advantages and use cases:

Advantages of Shadow DOM:

Use Cases for Shadow DOM:


I have prepared more examples with Shadow DOM https://github.com/Alex-1557/JsClosureAndAsync/tree/main/AI/AiShadowDom


3. Shadow DOM examples.

This is examples what illustrates how to use Shadow DOM:

1. Imagine you're building a date picker component to be used across your application. Without Shadow DOM, you might style it like this:


   1:  /* Global styles */
   2:  .date-picker {
   3:    border: 1px solid #ccc;
   4:    /* ... other styles ... */
   5:  }
   6:  .date-picker .calendar {
   7:    /* ... calendar-specific styles ... */
   8:  }
   9:  .date-picker .controls {
  10:    /* ... control button styles ... */
  11:  }

If another part of your application also uses classes like .calendar or .controls, but with different styling intentions, you'll run into conflicts. Your date picker's styling might bleed into other elements or vice-versa.

With Shadow DOM:


   1:  class DatePicker extends HTMLElement {
   2:    constructor() {
   3:      super();
   4:      this.attachShadow({mode: 'open'}); // Creates the Shadow DOM
   5:      const template = `
   6:        <style>
   7:          /* Styles scoped to the Shadow DOM */
   8:          .calendar {  /* No need for .date-picker prefix */
   9:             /* ... calendar styles ... */
  10:          }
  11:          .controls {  /* Also scoped */
  12:             /* ... controls styles ... */
  13:          }
  14:        </style>
  15:        <div class="calendar">...</div>
  16:        <div class="controls">...</div>
  17:      `;
  18:      this.shadowRoot.innerHTML = template;
  19:    }
  20:  }

Now, the .calendar and .controls styles within the Shadow DOM only apply to the date picker. External styles won't affect it, and the date picker's styles won't leak out. This makes your component truly self-contained and reusable without fear of style collisions.

2. Suppose you're integrating a third-party comment widget onto your page. Without Shadow DOM, the widget's CSS might clash with your site's CSS. Worse, JavaScript from the widget could inadvertently manipulate your page's DOM, causing unexpected behavior.

With Shadow DOM, you can contain the widget:


   1:  const commentWidget = document.createElement('div');
   2:  commentWidget.attachShadow({mode: 'open'});
   3:  commentWidget.shadowRoot.innerHTML = '...third-party widget code...';
   4:  document.body.appendChild(commentWidget); 

4. My case with Order/Donate component.

Now I want to return to case, while without WebComponent I can not resolved my case with Order/Donate block in my blog:

Firstly, historically, all part of pages was included as ASP.NET server component, than as Nginx tag like this:


   1:  <!-- #include virtual="/Front.htm" -->

But this blog hosted now as Cloudflare worker Project to move my blog from private VM with Cloudflare VPN to Google Drive and Cloudflare R2 storage and all server tag replaced to custom tag like this:


   1:  <Front></Front><script type="text/javascript">fetch("/Front.htm").then(response => response.text()).then(data => document.querySelector("Front").innerHTML = data)</script>

And if we perform old version of Order/Donate component:


   1:  <div id="order" style="text-align:right; display:inline; float:right; padding-left: 10px;">
   2:      <style>
   3:          .order-container {
   4:              white-space: nowrap;
   5:          }
   6:          a.order-button {
   7:              padding-top: 1px;
   8:              height: 35px !important;
   9:              box-shadow: 1px 1px 0px rgba(0, 0, 0, 0.2);
  10:              line-height: 36px !important;
  11:              min-width: 150px;
  12:              display: inline-block !important;
  13:              background-color: #29abe0;
  14:              text-align: center !important;
  15:              border-radius: 7px;
  16:              color: #fff;
  17:              cursor: pointer;
  18:              overflow-wrap: break-word;
  19:              vertical-align: middle;
  20:              border: 0 none #fff !important;
  21:              font-family: 'Quicksand', Helvetica, Century Gothic, sans-serif !important;
  22:              text-decoration: none;
  23:              text-shadow: none;
  24:              font-weight: 700 !important;
  25:              font-size: 14px !important;
  26:          }
  27:          a.order-button:visited {
  28:              color: #fff !important;
  29:              text-decoration: none !important;
  30:          }
  31:          .ExtLink {
  32:             height: 37px;
  33:           }
  34:           .orderimage {
  35:              position: relative;
  36:              top: 4px;
  37:              left: -5px;
  38:           }
  39:      </style>
  40:      <div class="order-container">
  41:          <a title="Order a job" class="order-button" style="background-color:#e028cc;"
  42:              href="https://jobrequest1.viacheslavdev0.workers.dev/" target="_blank">
  43:              <span class="ordertext">
  44:                  <img src="/Usd.png" alt="Order a job" class="orderimage">Order a job
  45:              </span>
  46:          </a>
  47:      </div>
  48:  </div>
  49:  <div id="coffee" style="text-align:right; display:inline; float:right;"></div>
  50:  <script type='text/javascript' src='https://storage.ko-fi.com/cdn/widget/Widget_2.js'></script>
  51:  <script type='text/javascript'>kofiwidget2.init('Buy me a coffee', '#2884e0', 'I2I1UJCBL'); kofiwidget2.draw();</script>
  52:   

We receive fantastic situation: on main page we can see only Order button, but in Include page we can see only Donate button.



5. Reason of trouble.

But why two button successfully worked as server tag, but stop working as JS tag? Reason is a common issue with dynamically loaded scripts and timing. Even though the Ko-fi widget script after the DOM is loaded, there's still a race condition. The widget initialization code (kofiwidget2.init(...)) might be executing before the Ko-fi script has fully downloaded and initialized kofiwidget2 itself. This is why page works when you directly load /Donate.htm (the browser has more time to load the script), but not when it's fetched and inserted dynamically.

First solution is delay, something like this:


   1:  .then(() => {
   2:      setTimeout(() => {  // Introduce a delay
   3:          kofiwidget2.init('Buy me a coffee', '#2884e0', 'I2I1UJCBL');
   4:          kofiwidget2.draw();
   5:      }, 1500); // 1500ms delay. Adjust as needed.
   6:  })

But, of course, any delay while page loading is not a best choice. So, this is a case where only WebComponent can help me, because firstly I need redefine script loading type from Async to Sync and than I need to inject completed DOM to main page.

6. My first solution with Shadow DOM.

My first solution was with Shadow DOM, I try to inject both button in the same way:


   1:  class DonateComponent extends HTMLElement {
   2:      connectedCallback() {
   3:          this.attachShadow({ mode: 'open' });  
   4:          this.render(); 
   5:          this.loadKofiWidget();
   6:          
   7:          this.addOrderButton()
   8:      }
   9:   
  10:      render() {  
  11:          this.shadowRoot.innerHTML = `
  12:            <div id="donate-content">
  13:             <div id="order" style="text-align:right; display:inline; float:right; padding-left: 10px;">
  14:             <div class="order-container">
  15:                  <a title="Order a job" class="order-button" style="background-color:#e028cc;"
  16:                    href="https://jobrequest1.viacheslavdev0.workers.dev/" target="_blank">
  17:                  <span class="ordertext">
  18:                     <img src="Usd.png" alt="Order a job" class="orderimage">Order a job
  19:                  </span>
  20:                  </a>
  21:              </div>
  22:             </div>
  23:             <div id="coffee" style="text-align:right; display:inline; float:right;"></div>
  24:             </div>
  25:        `;
  26:   
  27:      }
  28:   
  29:      loadKofiWidget() {
  30:          const script = document.createElement('script');
  31:          script.src = 'https://storage.ko-fi.com/cdn/widget/Widget_2.js';
  32:          script.async = false; 
  33:          script.onload = () => {
  34:            if (typeof kofiwidget2 !== 'undefined') {
  35:              kofiwidget2.init('Buy me a coffee', '#2884e0', 'I2I1UJCBL', '#ko-fi-widget');
  36:              console.log("Ko-fi Widget Script Loaded");
  37:              kofiwidget2.draw();
  38:            } else {
  39:              console.error("kofiwidget2 is still undefined after script load!");
  40:            }
  41:          };
  42:          script.onerror = (error) => {
  43:            console.error("Error loading widget script:", error);
  44:          };
  45:          this.shadowRoot.appendChild(script);
  46:        }
  47:        addOrderButton() {
  48:          const orderButton = document.createElement('div');
  49:          orderButton.id = 'order-button';
  50:          orderButton.innerHTML = `
  51:            <a title="Order a job" class="order-button" style="background-color:#e028cc;"
  52:               href="https://jobrequest1.viacheslavdev0.workers.dev/" target="_blank">
  53:              <span class="ordertext">
  54:                <img src="Usd.png" alt="Order a job" class="orderimage">Order a job
  55:              </span>
  56:            </a>
  57:          `;
  58:          this.shadowRoot.getElementById('my-buttons-container').appendChild(orderButton);
  59:        }
  60:    }
  61:    customElements.define('donate-component', DonateComponent);

But unfortunately, this way is not working, because Coffee script already hide my Order Button. Therefore, finally, I decide to simplify solution and split Order Button and Coffee button to two separate modules and isolate Coffee script to iframe. There are a couple reasons to deny from Shadow DOM in this case - Coffee script extremely intensively manipulate DOM dynamically, therefore I need isolate Coffe script to iframe and I have an opportunity to split two separate buttons to two separate modules.


   1:  <div id="donate-iframe-container"><iframe src="/Donate/DonateFrame.htm" frameborder="0" width="200" height="50"></iframe></div>
   2:  <Order></Order><script type="text/javascript">fetch("/Order.htm").then(response => response.text()).then(data => document.querySelector("Order").innerHTML = data)</script>
   3:   

This is reason because in final solution Shadow DOM don't need to me, I have isolated one button from another. And you can see my final solution in this page - both button working.




Front context:



Comments ( )
Link to this page: http://www.vb-net.com/WebComponent/Index.htm
< THANKS ME>