Product Card Component Through Web Components
Tags: tutorial, javascript, html, css
Published: 2020-10-07 02:19:00

# __Web Components__
We will be creating a Product Card Component Utilizing Web Components. Web components encapsulates complex HTML code when creating custom UI controls. It utilizes three technologies (HTML, CSS, and JavaScript) to create custom elements that can be reused anywhere without fear of code collision.

### **The Basics**
We first need to create a custom element to begin are web component. to do that we create a class or function to specify our elements functionality. We will create a class named _ProductCardComponent_ that extends the _HTMLElement_ class.

```javascript
class ProductCardComponent extends HTMLElement{
    constructor(){
        super();
    }
}
...
```


To register our custom element we use the _CustomElementRegistry.define()_ method. this method takes two arguments and one optional. The first argument takes a hyphen separated string (kebab-case) as the name for custom element, it cannot be one word. We will name our element _product-card_. The second argument is the name of the class or function that defines the behavior of our element.
```javascript
...
window.customElements.define('product-card', ProductCardComponent);
```


### __The Shadow DOM__
One of the key parts to the encapsulation of web components is the Shadow DOM. The Shadow DOM provides a way to attach a hidden separated DOM to an element allowing the style, structure and behavior of the component to not clash with any of the other code on the page.

To attach a shadow DOM to our custom element we use the _Element.attachShadow()_ method in our constructor. It takes one argument, and object with key: mode and value: 'open' or 'closed'. Open allows you to access the shadow DOM with JavaScript written in the main page context.
```javascript
...
constructor(){
   super();
   this.attachShadow({mode: 'open'});
}
...
```
next we create an element that will contain the structure of our component. Then we attach that element to the shadow DOM.
```javascript
...
constructor(){
   super();
   this.template = document.createElement('template');
   this.template.innerHTML = ` `;

   this.attachShadow({mode: 'open'});
   this.shadowRoot.appendChild(this.template.content.cloneNode(true));
}
...
```
That is it for the setup and now we can begin to structure our component.

# __Product Card Component__
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9jtiedqo669kkk81o6nf.png)

The product card will display an image of the product that it will be representing. We will need to allow the user of our component to set the source of this image. It Will also have a main text area (which will display the Product name) a sub text area (which will display the product price) and a button whose text, text color, and background color we will allow the user to set. We will set value for each of these in our elements attributes and use the _innerHTML_ to set the name of the button.
```html
<product-card img='./source.jpg' 
   main='Product Name' 
   sub-one='$10.00'
   button-color='orange'
   button-text-color='black'> Button Text </product-card>
```
We'll then create variables in our constructor and set them to the value of the attributes.
```javascript
...
constructor(){
   super();
   this.template = document.createElement('template');

   this.main = this.getAttribute('main');
   this.img = this.getAttribute('img');
   this.subOne = this.getAttribute('sub-one');
   this.buttonColor = this.getAttribute('button-color');
   this.buttonTextColor = this.getAttribute('button-text-color');
   this.buttonText = this.innerHTML;
   
   this.template.innerHTML = ` 
   
   `;

   this.attachShadow({mode: 'open'});
   this.shadowRoot.appendChild(this.template.content.cloneNode(true));
}
...
```
Next we will begin to build our component by writing our code in a string and assign it to _template.innerHTML_. Our component will consist of two main blocks so we'll create two _divs_ to represent these blocks and wrap them in a container div. We'll give the container div a class name of _card-cont_ and the two wrapped _divs_ class names of _img-cont_ and _info-cont_. Then we'll create an _img_ element in the _img-cont_ div and a few div elements in the _info-cont_ div to contain the main text, sub text and button. Using the variables we created earlier we'll input the _src_ attribute for the _img_ element and the text for the main text, sub text and button text

```javascript
...
   this.template.innerHTML = `
   <div class='card-cont'>
      <div class='img-cont'>
         <img src='${this.img}' alt='${this.main}'/>
      </div>
      <div class='info-cont'>
         <div class='top-info-cont'>
            <div class='main-cont'><p>${this.main}<p></div>
            <div class='sub1-cont'><p>${this.subOne}<p></div>
         </div>
         <div class='bottom-button-cont'>
            <button>${this.buttonText}</button>
         </div>
      </div>
   </div>
`
...
```
Now to style the component we'll add a style element right above the _card-cont_ div.

```javascript
...
   this.template.innerHTML =`
   <style>
   </style>
   <div class='card-cont'>
      ...
   </div>
`
...
```
##### Button and container styles
```javascript
...
this.template.innerHTML= `
      <style>
         button{
             min-width: 7rem;
             height: 1.8rem;
             opacity: 0;
             transition: 100ms ease 0s;
             border-radius: .5rem;
             border: none;
             background-color: ${this.buttonColor} ;
             font-weight: 300;
             font-size: .7rem;
             color: ${this.buttonTextColor};
         }
         img{
             width: 100%;
             min-height: 100%;
         }
         .card-cont{
             font-family: Segoe UI, sans-serif;
             font-size: .98rem;
             position: relative;
             background-color: none;
             width: 16.3rem;
             height: 16rem;
             transition: 500ms;
             color: white;
         }
         .img-cont{
             background-color: grey;
             width: 15rem;
             height: 15rem;
             transition: 500ms;
             overflow: hidden;
         }
         .info-cont{
             position: absolute;
             background-color: black;
             width: 11.8rem;
             height: 1.2rem;
             top: 13.75rem;
             left: 2.5rem;
             border-radius: .6rem;
             transition: height 500ms;
             padding: .5rem 1rem;
             box-shadow: 0px 0px 8px rgba(1,1,1,.3);
         }
         .top-info-cont{
             display: flex;
             justify-content: center;
             min-height: 50%;
             width: 100%;
         }
         .bottom-button-cont{
             display: flex;
             justify-content: center;
             align-items: center;
             height: 50%;
             width: 100%;
         }
         .main-cont{
             display: flex;
             flex-wrap: nowrap;
             font-weight: 700;
             text-align: left;
             width: 70%;
              
         }
         .sub1-cont{
             font-size: .8rem;
             text-align: right;
             width: 30%;
                
         }
      ...
      </style>
...
`
```
##### Animation and interaction styles
```javascript
...
this.template.innerHTML =`
         ...
            .card-cont:hover{
                transform: scale(1.05,1.05);
                z-index: 100;
            }
            .card-cont:hover > .img-cont{
                border-radius: 1rem;
                box-shadow: 30px 30px 50px rgba(1,1,1,.3);
            }
            .card-cont:hover > .info-cont{
                height: 5.5rem;
                box-shadow: 30px 30px 50px rgba(1,1,1,.3);
            }
            .card-cont:hover top-info-cont{
                height: 50%;
            }
            .card-cont:hover button{
                opacity: 1;
                transition: 500ms ease-in 0s;
            }
            button:active{
                transform: scale(1.1,1.1);
            }
            .card-cont:focus{
                outline: 1px solid black;
                outline-offset: .5rem;
            }
         ...
`
...
```
That will complete our card component.

# __Extra__
Here are a few different modifications we can make to our component also using other web component features we have not yet talked about.
### Custom Events

To allow the user of our component to know when the button in our web component is clicked we can create a custom event that the user can then run an event listener for. To start we'll make a method in our elements class and call it _buttonClicked_. Inside the method well create a new custom event with the _CustomEvent()_ constructor and assign it to a _const_ named event. Custom event takes two arguments, the first a string representing the name of the event and the second(optional) an object with a key 'detail' of any value. detail can contain any data you want passed to the event listener. We then have the method dispatch the event.

```javascript
...
   buttonClicked(){
      const event = new CustomEvent('',{
         detail:{
            id:this.id
         }
      });
      document.dispatchEvent(event);
   }
...
```
### Web Components: connectedCallback and disconnectedCallback
_connectedCallback_ is called when the custom element is first connected to the documents DOM. _disconnectedCallback_ is called when disconnect from the documents DOM. We will use the _connectedCallback_ method to run an event listener for a 'click' on the components button and use _disconnectedCallback_ to remove that event listener.
```javascript
...
connectedCallback() {
   this.shadowRoot.querySelector('button').addEventListener('click', () => this.buttonClicked());
   }
disconnectedCallback() {
   this.shadowRoot.querySelector('button').removeEventListener();
   }
...
```
### Web Components: Slots
The _slot_ element is a place holder that you can fill with your own markup. It allows you to create separate Dom trees and present them together. We will remove the button in our _bottom-button-container_ _div_ and replace it with a _slot_ Element. then we will insert our elements _innerHTML_ into the slot element. This will allow the user of our web component to put there own markup into the _info-cont_ area of our component.
```javascript
this.main = this.getAttribute('main');
   this.img = this.getAttribute('img');
   this.subOne = this.getAttribute('sub-one');
   this.buttonColor = this.getAttribute('button-color');
   this.buttonTextColor = this.getAttribute('button-text-color');
   this.userMrkUp = this.innerHTML;
 
   this.template.innerHTML = `
...
   <div class='card-cont'>
      <div class='img-cont'>
         <img src='${this.img}' alt='${this.main}'/>
      </div>
      <div class='info-cont'>
         <div class='top-info-cont'>
            <div class='main-cont'><p>${this.main}<p></div>
            <div class='sub1-cont'><p>${this.subOne}<p></div>
         </div>
         <div class='bottom-button-cont'>
            <slot>${this.userMrkUp}</slot>
         </div>
      </div>
   </div>
`
...
```
[Code](https://github.com/LE4u5/Product-Card-Component.git)