Encapsulate HTML, CSS, and JS to WebComponent. Shadow DOM. Example of my WebComponent in this blog.
- 1. My current WebComponent
- 2. WebComponent and Shadow DOM definition.
- 3. Shadow DOM examples.
- 4. My case with Order/Donate component.
- 5. Reason of trouble.
- 6. My first solution with Shadow DOM.
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:
- • Functionality: A simple JavaScript script is a piece of code that you embed in your HTML using <script> tags. It can perform various tasks, such as manipulating the DOM (Document Object Model—the structure of your HTML page), handling user events (clicks, form submissions, etc.), making network requests, and more.
- • Encapsulation: Simple scripts often lack strong encapsulation. Their variables and functions might directly affect global variables or the DOM without clear boundaries or protection. This can lead to conflicts if you have multiple scripts that interact with the same DOM elements or variables.
- • Reusability: Reusability is limited. While you can write functions that you reuse, this typically involves manually copying and pasting the code or creating separate JavaScript modules that you then import into multiple pages; they do not have their own, well-defined lifecycle or DOM-handling.
- • Lifecycle: There is no defined lifecycle, beyond a simple onload event. You have to manually write code to handle initialization, DOM manipulation, and cleanup.
Web Component:
- • Functionality: A Web Component is a custom HTML element that you define using JavaScript. It encapsulates HTML, CSS, and JavaScript into a reusable unit, significantly enhancing code organization, reusability, and maintainability.
- • Structure: A Web Component is defined using a class that extends HTMLElement. Within the class, you define:
- ◦ connectedCallback(): This function is called automatically when the web component is added to the DOM (the web page's structure). It is where initialization code (such as fetching data, initializing variables, adding event listeners, etc.) is placed.
- ◦ disconnectedCallback(): This is called when the component is removed from the DOM. Use it to handle cleanup, such as removing event listeners.
- ◦ Template Literal: Often, a web component's structure is defined within a template literal (``` `) as a string, providing cleaner HTML within the Javascript code, especially if your component is complex and includes many nested HTML elements and attributes.
- ◦ shadowRoot: Web components typically use a shadowRoot. The shadow DOM is a distinct part of the page that is isolated from the main page's DOM. It protects the component's styles and layout from conflicting with other parts of the page and avoids interference issues from other elements or styles on the page. This is essential for building reusable and maintainable components that won't unexpectedly break if another component adds elements or styles to the page.
- • Encapsulation: Web Components have strong encapsulation. Their internal state (data and variables) and styling (CSS) are isolated from the rest of the page. This makes your code much easier to maintain and reduces the risk of unexpected conflicts between different parts of your application.
- • Reusability: Web Components are highly reusable. You define them once and use them anywhere within your website. They are much easier to reuse than simple JS scripts, because they don't require manual management of initialization, DOM manipulation, styles, or handling of lifecycle events.
- • Lifecycle: Web Components have a well-defined lifecycle. connectedCallback() and disconnectedCallback() make it easier to write well-structured and reliable code for reusable components.
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:
- • Style Encapsulation: Styles defined within the Shadow DOM are scoped to the component and don't affect the main document's styles or vice versa. This eliminates the risk of style collisions and makes components more self-contained.
- • DOM Encapsulation: The Shadow DOM creates a separate DOM tree for the component, hiding its internal structure from the main document. This prevents accidental modification of the component's DOM by external scripts. It also simplifies the component's structure as you don't have to worry about naming conflicts with the main page.
- • Composition: Shadow DOM makes it easier to build complex components by combining smaller, reusable components. Since the internal structure of each component is hidden, there's less chance of conflicts.
- • Performance: Shadow DOM can improve performance by reducing the number of elements in the main DOM tree. This can be particularly beneficial for large applications with many components. Because styles are scoped, the browser only has to recalculate styles within the shadow DOM when changes occur, rather than re-evaluating the entire page.
Use Cases for Shadow DOM:
- • Building Reusable Web Components: Shadow DOM is essential for creating reusable and self-contained web components that can be easily integrated into different parts of an application or even across different projects.
- • Third-party Widgets and Embeds: When integrating third-party widgets or content, Shadow DOM provides a way to isolate the widget's code from the main page, preventing conflicts and maintaining security.
- • Design Systems: Create consistent and reusable UI elements without the worry of style collisions. This allows development teams to create a central library of components used throughout a product or across various products.
- • Complex UI Frameworks/Libraries: Many modern frameworks and libraries leverage Shadow DOM to encapsulate their components’ logic. This makes debugging and updating more manageable.
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:
|