Você sabe por quê programadores não gostam de temas claros? Porque luz atrai insetos!... Insetos... BUGS... Sacou?... Desculpa.
Pra fazer esse blog, eu decidi que queria a capacidade de trocar o tema de cores entre claro e escuro. Então, aproveitei a deixa para treinar um pouquinho de Web Components, e criei um pequeno componente para essa tarefa.
O resultado ficou simples e funcional, mas uma coisa estava me incomodando neste trecho de código aqui:
// theme-changer-component.js
export default class ThemeChanger extends HTMLElement {
...
createDOMTree() {
const lang = this.getAttribute('lang');
const lightThemeButton = document.createElement('button');
lightThemeButton.classList.add('light-theme-button');
lightThemeButton.type = 'button';
lightThemeButton.innerText = 'A';
lightThemeButton.title =
lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme';
this.lightThemeButton.addEventListener(
'click',
this.handleLightThemeButtonClick.bind(this)
);
const darkThemeButton = document.createElement('button');
darkThemeButton.classList.add('dark-theme-button');
darkThemeButton.type = 'button';
darkThemeButton.innerText = 'A';
darkThemeButton.title =
lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme';
this.darkThemeButton.addEventListener(
'click',
this.handleDarkThemeButtonClick.bind(this)
);
const lightThemeButtonContainer = document.createElement('div');
lightThemeButtonContainer.classList.add('icon-container');
lightThemeButtonContainer.append(lightThemeButton);
const darkThemeButtonContainer = document.createElement('div');
darkThemeButtonContainer.classList.add('icon-container');
darkThemeButtonContainer.append(darkThemeButton);
const wrapper = document.createElement('div');
wrapper.ariaHidden = true;
wrapper.classList.add('wrapper');
wrapper.append(lightThemeButtonContainer);
wrapper.append(darkThemeButtonContainer);
return wrapper;
}
...
}
Eu não conseguia parar de pensar: "Olha a quantidade de vezes que estou criando um elemento manualmente, depois setando atributos manualmente! Tem que haver um jeito mais conciso de fazer isso."
Então achei que era uma boa ideia escrever um pequeno utilitário para criar elementos HTML:
// createElement.js
export const a = new Proxy({}, {
get: (target, name) => {
if (!target[name])
target[name] = (
properties => createElement(name, properties)
);
return target[name];
},
});
function createElement(name, properties) {
const element = document.createElement(name);
for (let key in properties) {
if (key === 'children') {
element.append(...properties[key])
continue;
}
element[key] = properties[key];
}
return element;
}
Este utilitário permite criar elementos mais ou menos assim:
// theme-changer-component.js
import { a } from './createElement.js';
export default class ThemeChanger extends HTMLElement {
...
createDOMTree() {
...
// antes
const lightThemeButton = document.createElement('button');
lightThemeButton.classList.add('light-theme-button');
lightThemeButton.type = 'button';
lightThemeButton.title = lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme';
lightThemeButton.innerText = 'A';
// depois
const lightThemeButton = a.button({
classList: ['light-theme-button'],
type: 'button',
title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
innerText: 'A',
});
...
}
...
}
Interessante! Esse código parece ter ficado bom e precisei digitar um pouco menos quando comparado à API nativa do DOM.
Uau! Essa ideia foi realmente muito esperta!
Agora, ao reescrever o código aplicando este utilitário, o resultado fica assim:
// theme-changer-component.js
import { a } from './createElement.js';
export default class ThemeChanger extends HTMLElement {
...
createDOMTree() {
const lang = this.getAttribute('lang');
this.lightThemeButton = a.button({
classList: ['light-theme-button'],
type: 'button',
innerText: 'A',
title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
onclick: this.handleLightThemeButtonClick.bind(this),
});
this.darkThemeButton = a.button({
classList: ['dark-theme-button'],
type: 'button',
innerText: 'A',
title: lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme',
onclick: this.handleDarkThemeButtonClick.bind(this),
});
this.lightThemeButtonContainer = a.div({
classList: ['icon-container'],
children: [this.lightThemeButton],
});
this.darkThemeButtonContainer = a.div({
classList: ['icon-container'],
children: [this.darkThemeButton],
});
const wrapper = a.div({
ariaHidden: true,
classList: ['wrapper'],
children: [
this.lightThemeButtonContainer,
this.darkThemeButtonContainer
],
});
return wrapper;
}
...
}
Que vantagens obtemos com essa nova implementação? Bom, consigo pensar somente em 2 coisas:
- Digitamos um pouquinho menos de código, mas isso por si só não tem muito valor e a diferença também não é muito grande
- O código talvez esteja mais fácil de ler. Mas a forma como o código foi escrito anteriormente era tão difícil de ler a ponto de justificar essa mudança? Acredito que não
Mas e se irmos um pouquinho mais além? Vamos tentar aninhar a criação dos elementos e avaliar estes dois pontos novamente:
// theme-changer-component.js
import { a } from './createElement.js';
export default class ThemeChanger extends HTMLElement {
...
createDOMTree() {
const lang = this.getAttribute('lang');
const wrapper = a.div({
ariaHidden: true,
classList: ['wrapper'],
children: [
a.div({
classList: ['icon-container'],
children: [
a.button({
classList: ['light-theme-button'],
type: 'button',
innerText: 'A',
title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
onclick: this.handleLightThemeButtonClick.bind(this),
}),
],
}),
a.div({
classList: ['icon-container'],
children: [
a.button({
classList: ['dark-theme-button'],
type: 'button',
innerText: 'A',
title: lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme',
onclick: this.handleDarkThemeButtonClick.bind(this),
}),
],
}),
],
});
return wrapper;
}
...
}
Agora, o código parece menos legível, além de termos múltiplos níveis de indentação. E mesmo que alguém ache isso mais legível, se resume a um gosto pessoal e, além do mais, ainda precisamos falar sobre as desvantagens do novo código:
-
Uma pessoa lendo o novo código vai ter uma maior carga cognitiva por ter de entender o funcionamento
da nova função
a
ao invés de ter de lidar somente com a API nativa do DOM - Criar bibliotecas e utilitários dessa forma demanda uma grande quantidade de testes para evitar a introdução de novos bugs. Se eu avançar muito na funcionalidade do utilitário, é mais fácil simplesmente utilizar alguma bilbioteca já pronta, como o React.
- E a maior desvantagem de todas: Perdemos quase todo o suporte da IDE: O novo código nos faz perder autocomplete, detecção de erros e toda e qualquer ferramenta que trabalhe analisando as API do DOM. Tudo o que conseguimos da IDE agora são sugestões fora de contexto.
Inicialmente, a ideia de criar um utilitário para criar elementos HTML pareceu ser muito boa, mas após um pouco de análise, chegamos à conclusão que a tentativa de economizar código trouxe mais prejuízos do que benefícios.
Qual lição fica disso? Simples: Não tente reinventar a roda e não crie códigos "espertinhos" 😉