WebDev Bites: ImageViewer Component Design

W3C web component

Component Code

The tag <image-viewer> creates an instance of the ImageViewer class. The class definition extends from lines 1-94. Component attributes are defined in lines 7-14. Attributes allow user applications to modify encapsulated component values. shadowDOM is defined in lines 17-80. It includes styles, lines 18-70 and HTML element structure in lines 72-79. Event listeners are defined in lines 83-87, that support expanding the view by clicking on img body and contracting view by clicking on title. The Helper function resizeImage(scaleFactor) is defined in lines 90-93.

Shadow DOM

A W3C web component is defined entirely in JavaScript as illustrated in the right panel. The shadowDOM provides styling and HTML markup for the component using a JavaScript expression: this.shadowRoot.innerHTML = `[markup goes here]`; . Note that a style block is placed near the top of the component which lies in the component element of the body, not the head. This is done to encapsulate the component styles so that they are not corrupted by styles used in the application, perhaps in an included style library.

Structure

This markup defines the structure of the image-viewer component. The outer div provides padding to keep content from touching the image-viewer when it is floated to the left or right. The next div provides the visible part of the component, e.g., its title and image. Note that the img src and width are provided by attributes the application supplies in the <image-viewer> declaration.

Listeners

The primary purpose of this component is to enlarge or diminish the size of an image using button clicks on image (enlarge) or title (diminish). The first listener awaits clicks on the title which contracts the image. The second awaits clicks on the image then enlarges it. The final piece registers the <image-viewer> with this component class. That causes the browser's rendering engine to create an instance of the ImageViewer class for each occurance of the tag.

 1 class ImageViewer extends HTMLElement {
 2   constructor() {
 3     super();
 4     this.attachShadow({ mode: 'open' });
 5
 6     // Get attributes
 7     this.imgSrc = this.getAttribute('img-src') || '';
 8     this.imgWidth = this.getAttribute('img-width') || '400';
 9     const hasBgAttr = this.hasAttribute('bg-color');
10     const componentBg = hasBgAttr ? this.getAttribute('bg-color') : 'white';
11     const titleBg = this.getAttribute('title-bg-color') || 'transparent';
12
13     // Wrapper background uses --light fallback to white
14     const wrapperBg = 'var(--light, white)';
15
16     // Shadow DOM markup
17     this.shadowRoot.innerHTML = `
18       <style>
19         :host {
20           display: inline-block; /* allow float and shrink to content */
21         }
22
23         .wrapper {
24           padding: 1rem; /* outer padding to keep text off the border when floated */
25           box-sizing: border-box;
26           background-color: ${wrapperBg};
27         }
28
29         .component {
30           border: 2px solid var(--dark, #333); /* use client's --dark or fallback */
31           padding: 0.5rem;
32           display: flex;
33           flex-direction: column;
34           user-select: none;
35           width: min-content;
36           box-shadow: 5px 5px 5px #999;
37           box-sizing: border-box;
38           background-color: ${componentBg};
39         }
40
41         .title {
42           display: flex;
43           font-family: "Comic Sans MS", cursive, sans-serif;
44           font-weight: bold;
45           cursor: pointer;
46           max-width: 100%;
47           margin-bottom: 8px;
48           line-height: 1.0rem;
49           flex-wrap: wrap;
50           word-wrap: break-word;
51           overflow-wrap: break-word;
52           white-space: wrap;
53           color: var(--dark, #333); /* title text uses --dark or fallback */
54           background-color: ${titleBg};
55           padding: 0.25rem 0.5rem;
56         }
57
58         .image {
59           display: block;
60           flex: 0 0 auto;
61           cursor: pointer;
62           transition: transform 0.2s ease-in-out;
63         }
64
65         img {
66           display: block;
67           height: auto;
68           /* removed max-width so explicit width adjustments take effect */
69         }
70       </style>
71
72       <div class="wrapper">
73         <div class="component">
74           <div class="title" part="title"><slot></slot></div>
75           <div class="image">
76             <img id="img" src="${this.imgSrc}" width="${this.imgWidth}">
77           </div>
78         </div>
79       </div>
80     `;
81
82     // Event listeners
83     this.titleElement = this.shadowRoot.querySelector('.title');
84     this.imageElement = this.shadowRoot.querySelector('#img');
85
86     this.titleElement.addEventListener('click', () => this.resizeImage(1 / 1.2));
87     this.imageElement.addEventListener('click', () => this.resizeImage(1.2));
88   }
89
90   resizeImage(scaleFactor) {
91     const currentWidth = parseFloat(window.getComputedStyle(this.imageElement).width);
92     this.imageElement.style.width = `${currentWidth * scaleFactor}px`;
93   }
94 }
95
96 customElements.define('image-viewer', ImageViewer);

16     // Shadow DOM markup
17     this.shadowRoot.innerHTML = `
18       <style>
19         :host {
20           display: inline-block; /* allow float and shrink content */
21         }
22
23         .wrapper {
24           padding: 1rem; /* outer padding to keep text off the border when floated */
25           box-sizing: border-box;
26           background-color: ${wrapperBg};
27         }
28
29         .component {
30           border: 2px solid var(--dark, #333); /* use client's --dark or fallback */
31           padding: 0.5rem;
32           display: flex;
33           flex-direction: column;
34           user-select: none;
35           width: min-content;
36           box-shadow: 5px 5px 5px #999;
37           box-sizing: border-box;
38           background-color: ${componentBg};
39         }
40
41         .title {
42           display: flex;
43           font-family: "Comic Sans MS", cursive, sans-serif;
44           font-weight: bold;
45           cursor: pointer;
46           max-width: 100%;
47           margin-bottom: 8px;
48           line-height: 1.0rem;
49           flex-wrap: wrap;
50           word-wrap: break-word;
51           overflow-wrap: break-word;
52           white-space: wrap;
53           color: var(--dark, #333); /* title text uses --dark or fallback */
54           background-color: ${titleBg};
55           padding: 0.25rem 0.5rem;
56         }
57
58         .image {
59           display: block;
60           flex: 0 0 auto;
61           cursor: pointer;
62           transition: transform 0.2s ease-in-out;
63         }
64
65         img {
66           display: block;
67           height: auto;
68           /* removed max-width so explicit width adjustments take effect */
69         }
70       </style>
71

72       <div class="wrapper">
73         <div class="component">
74           <div class="title" part="title"><slot></slot></div>
75           <div class="image">
76             <img id="img" src="${this.imgSrc}" width="${this.imgWidth}">
77           </div>
78         </div>
79       </div>
80     `;
81

82     // Event listeners
83     this.titleElement = this.shadowRoot.querySelector('.title');
84     this.imageElement = this.shadowRoot.querySelector('#img');
85
86     this.titleElement.addEventListener('click', () => this.resizeImage(1 / 1.2));
87     this.imageElement.addEventListener('click', () => this.resizeImage(1.2));
88   }
89
90   resizeImage(scaleFactor) {
91     const currentWidth = parseFloat(window.getComputedStyle(this.imageElement).width);
92     this.imageElement.style.width = `${currentWidth * scaleFactor}px`;
93   }
94 }
95
96 customElements.define('image-viewer', ImageViewer);