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.
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.
...
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.
...
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.
...
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
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.
<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.
...
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
...
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.
...
this.template.innerHTML =`
<style>
</style>
<div class='card-cont'>
...
</div>
`
...
Button and container styles
...
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
...
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.
...
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.
...
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.
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>
`
...
Top comments (0)