In the last three weeks I've been switching my web app from classic HTML+CSS+JS to web components. In the begening it was all roses. So good to work with: 1. Encapsulating, 2. Reusing, 3. Less javascript code to be written, 4. Refactor one, refactor all, 5. UI clean and nice, 6. Easy to positioning and grouping. etc.
As I got excited with them, I started to change everything to web components and nesting one inside another... Then suddendly it turned into a true hell... So much more work do do in order to make the components interact with each other, use my APIs inside the shadowRoot, etc etc... Finilly I manage to make it all work together.
Here is an exemple of one of my popups:
<s-popup id="userPanelPOP" placeholder="Painel do Usuário">
<div class="flexCenter" loading="lazy">
<div class="popupPageExt pdg10">
<!-- User Panel Header -->
<div class="w100 flex" style="height: 65px;">
<!-- ProfilePicture Container -->
<div class="h100 w100 flexCenter" id="profilePictureUserPanelBOX">
<div class="rel">
<svg-hexed-image class="profilePic"></svg-hexed-image>
<div class="abs changePhotoBox">
<div class="rel profilePhotoIconBanner">
<i class="bi bi-camera-fill abs profilePhotoIcon"></i>
<input class="inputFile" type="file" onchange="uploadProfilePicture(this)">
</div>
</div>
</div>
</div>
<!-- Status Container -->
<div class="h100 w100 flexCenter" id="statusUserPanelBOX">
<div class="balloon" id="userPanelAuthBox">
<i class="bi bi-exclamation-octagon-fill userPanelMenssager"></i>
</div>
</div>
<!-- Logout Container -->
<div class="h100 w100 rel">
<s-button class="abs posTR" id="logoutUserPanelBTN" model="danger" height="30px">
<div class="centerItemsH">
<i class="bi bi-box-arrow-left"></i>
<div style="min-width: 4px;"></div>
Sair
<div style="min-width: 1px;"></div>
</div>
</s-button>
</div>
</div>
<!-- Welcome Menssage -->
<s-text class="mgnTB10" id="welcomeUserPanelTXT"></s-text>
<!-- Selling / Buying -->
<div class="w100 flex mediaW380InlineBOX">
<!-- My Store -->
<div class="flexCenter flexRight w100">
<s-button class="w100" id="myStoreUserPanelBTN" model="disable">
<div class="centerItemsH">
<i class="bi bi-shop fs24"></i>
<div class="w10"></div>
Minha Loja
</div>
</s-button>
</div>
<!-- Divisor -->
<div class="mediaW380InlineDIV" style="min-width:10px;"></div>
<!-- My Purchases Historic -->
<div class="flexCenter flexLeft w100">
<s-button class="w100" id="myPurchasesUserPanelBTN">
<div class="centerItemsH">
<i class="bi bi-clock-history fs24"></i>
<div class="w10"></div>
Minhas Compras
</div>
</s-button>
</div>
</div>
<!-- User Informations -->
<s-topic class="mgnT10" id="userInformationsTPC" icon="bi-arrow-down-right-square" placeholder="Informações do Usuário" display="none" style="min-width:212px">
<div class="popupPage">
<s-input
class="mgnT5"
id="nameUserPanelINP"
placeholder="Nome Completo:"
maxlength="80"
charCounter="true"
model="edit"
regexAllow="FULL_NAME"
>Nome Completo</s-input>
<s-input
id="emailUserPanelINP"
placeholder="Email:"
inputmode="email"
maxlength="80"
charCounter="true"
model="edit"
regexAllow="EMAIL"
>Email</s-input>
<div class="flex">
<div class="w100">
<s-input
id="birthUserPanelINP"
placeholder="Data de Nascimento:"
inputmode="numeric"
maxlength="10"
model="edit"
regexAllow="BIRTH"
>Data de Nascimento</s-input>
</div>
<div class="mw10"></div>
<div class="w100">
<s-input
id="telefonUserPanelINP"
placeholder="Telefone:"
inputmode="numeric"
maxlength="14"
model="edit"
regexAllow="TELEFON"
>Telefone</s-input>
</div>
</div>
<!-- Gender Row -->
<div class="flex">
<div class="w100">
<s-input
id="cpfUserPanelINP"
placeholder="CPF:"
model="edit"
maxlength="14"
inputmode="numeric"
regexAllow="CPF"
>CPF</s-input>
</div>
<div class="mw10"></div>
<div class="w100">
<s-input
id="passwordUserPanelINP"
placeholder="Nova Senha:"
model="edit"
maxlength="60"
>Senha</s-input>
</div>
</div>
<s-input
class="disNone"
id="firstPasswordUserPanelINP"
placeholder="Primeira Senha:"
maxlength="60"
type="password"
>Primeira Senha</s-input>
<s-input
class="disNone"
id="newPassword1UserPanelINP"
placeholder="Nova Senha:"
maxlength="60"
type="password"
>Nova Senha</s-input>
<s-input
class="disNone"
id="newPassword2UserPanelINP"
placeholder="Nova Senha:"
maxlength="60"
type="password"
>Nova Senha</s-input>
<div class="flex">
<div class="w100 flexCenter">
<s-checkbox class="mgn10" id="womanUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Mulher</s-checkbox>
</div>
<div class="w100 flexCenter">
<s-checkbox class="mgn10" id="manUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Homem</s-checkbox>
</div>
<div class="w100 flexCenter">
<s-checkbox class="mgn10" id="otherUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Outro</s-checkbox>
</div>
</div>
<!-- User Addresses -->
<s-topic id="userAddressesTPC" icon="bi-arrow-down-right-square" placeholder="Endereços" display="none">
<div class="popupPage">
<s-box placeholder="Novo Endereço" id="newAddressBOX">
<s-topic class="mgnT5" id="closeNewAddressUserPanelTPC" icon="bi-dash-square-dotted" placeholder="Excluir Novo Endereço">
<s-input
id="nameNewAddressUserPanelINP"
placeholder="Nome do Endereço (Ex.:Casa):"
maxlength="80"
charCounter="true"
regexAllow="FULL_NAME"
>Email</s-input>
<div class="flex">
<div class="w100">
<s-input
id="cepNewAddressUserPanelINP"
placeholder="CEP:"
inputmode="numeric"
maxlength="10"
regexAllow="CPF"
>CEP</s-input>
</div>
<div class="mw10"></div>
<div class="w100">
<s-input
id="stNumberNewAddressUserPanelINP"
placeholder="Nº do Endereço:"
inputmode="numeric"
maxlength="7"
regexAllow="NUMBER"
>Nº do Endereço</s-input>
</div>
</div>
<s-input
id="streetNewAddressUserPanelINP"
placeholder="Rua:"
maxlength="80"
charCounter="true"
regexAllow="FULL_NAME"
>Rua</s-input>
<s-input
id="districtNewAddressUserPanelINP"
placeholder="Bairro:"
maxlength="80"
charCounter="true"
regexAllow="FULL_NAME"
>Bairro</s-input>
<div class="flex w100">
<div class="w100">
<s-input
id="cityNewAddressUserPanelINP"
placeholder="Cidade:"
maxlength="40"
regexAllow="FULL_NAME"
>CEP</s-input>
</div>
<div class="mw10"></div>
<div style="max-width:60px;min-width:60px;">
<s-input
id="stateNewAddressUserPanelINP"
maxlength="2"
regexAllow="FULL_NAME"
>Estado</s-input>
</div>
</div>
<s-input
id="complementNewAddressUserPanelINP"
placeholder="Complemento (opcional):"
maxlength="80"
charCounter="true"
regexAllow="FULL_NAME"
>Complemento</s-input>
<s-button class="w100 mgnT10" id="submitNewAddressUserPanelBTN" model="disable">Cadastrar Novo Endereço</s-button>
</s-box>
<s-topic class="mgnT10" id="addNewAddressUserPanelTPC" icon="bi-plus-square-dotted" placeholder="Adicionar Endereço"></s-topic>
</div>
</s-topic>
</div>
</s-topic>
<s-input
class="disNone"
id="actualPasswordUserPanelINP"
placeholder="Senha Atual:"
maxlength="60"
type="password"
>Senha Atual</s-input>
<s-button class="disNone mgnTB10" id="updateUserInfoUserPanelBTN">Atualizar Informações</s-button>
</div>
</div>
</s-popup>
In the end, It took a fair amount of time to migrate completly and as I was switching to web components, I notice that the page loading was getting really slower. Google lighthouse performance score dropped from 90 to 40...
Even I tryed to lazy loading, lazy rendering, I cutted the JS into bundles and delivery them as requested, but I did not feel it was a good way to work with a progressive web app...
Now I do not know if it was worthy... I feel I lost too much time "running in circles" ending up with a much slower page...
Should I keep using web-components or just go to the old and fast way to develop an UI?
I found the solution!!! By doing some more research, I understand now that nesting web components is not a good ideia by using innerHTML like this:
Old <s-popup>
class Popup extends HTMLElement {
constructor(){
super();
// Init ShadowRoot
const shadowRoot = this.attachShadow({ mode: 'open' });
// Add class to all popups
this.classList.add('popup');
this.classList.add('disNone');
// Get tag attributes
const id = this.getAttribute('id')??'';
const placeholder = this.getAttribute('placeholder')??'';
const content = this.innerHTML??'';
// Design HTML component
shadowRoot.innerHTML = `
<style>
@import url('./assets/style.min.css');
@import url('./assets/~bootstrap-icons/font/bootstrap-icons.css');
:host{ display: flex; }
</style>
<div class="popupWrap1">
<div class="popupWrap2">
<div class="popupWrap3 popupCloser">
<div class="popupWrap4 popupCloser">
<div class="popupDialog popupFilm" onclick="event.stopPropagation();">
<div class="popupTitle flexCenter">
${placeholder}
<i class="bi bi-square-fill bgSquareClose"></i>
<i class="bi bi-x-square-fill popupCloseIcon" onclick="closePopup(${id});"></i>
<div class="popupReturn flexCenter"></div>
<div class="titleBottomBorder"></div>
</div>
${content}
</div>
</div>
</div>
</div>
</div>`;
// Popup closer
this.shadowRoot.querySelector('.popupCloser').onclick = e =>{ closePopup(this); }
}
hideNavBars(){
this.shadowRoot.querySelector('.popupWrap1').style.marginTop = '0px';
this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = '0px';
}
showNavBars(){
this.shadowRoot.querySelector('.popupWrap1').style.marginTop = nbHeaderHeight + 'px';
this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = nbFooterHeight + 'px';
}
}
customElements.define('s-popup', Popup);
but simply using <slot>
tag instead of innerHTML did the trick and now the page is loading blazingly fast!
New <s-popup>
class Popup extends HTMLElement {
constructor(){
super();
// Init ShadowRoot
const shadowRoot = this.attachShadow({ mode: 'open' });
// Add class to all popups
this.classList.add('popup');
this.classList.add('disNone');
// Get tag attributes
const id = this.getAttribute('id')??'';
const placeholder = this.getAttribute('placeholder')??'';
// Design HTML component
shadowRoot.innerHTML = `
<style>
@import url('./assets/style.min.css');
@import url('./assets/~bootstrap-icons/font/bootstrap-icons.css');
:host{ display: flex; }
</style>
<div class="popupWrap1">
<div class="popupWrap2">
<div class="popupWrap3 popupCloser">
<div class="popupWrap4 popupCloser">
<div class="popupDialog popupFilm" onclick="event.stopPropagation();">
<div class="popupTitle flexCenter">
${placeholder}
<i class="bi bi-square-fill bgSquareClose"></i>
<i class="bi bi-x-square-fill popupCloseIcon" onclick="closePopup(${id});"></i>
<div class="popupReturn flexCenter"></div>
<div class="titleBottomBorder"></div>
</div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>`;
// Popup closer
this.shadowRoot.querySelector('.popupCloser').onclick = e =>{ closePopup(this); }
}
hideNavBars(){
this.shadowRoot.querySelector('.popupWrap1').style.marginTop = '0px';
this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = '0px';
}
showNavBars(){
this.shadowRoot.querySelector('.popupWrap1').style.marginTop = nbHeaderHeight + 'px';
this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = nbFooterHeight + 'px';
}
}
customElements.define('s-popup', Popup);
simple like that!
The reason is when nesting web components using innerHTML, every time we render each inner-nested-component, the whole DOM needs to be updated as innerHTML manipulates directly the DOM. But by using <slot>
instead, the browser creates a Virtual DOM, then renders the page inside of it using a some sort of fast memory allocation and in the end, it just does a simple and single update to the DOM. Now it is working like a charm!