Do you know why programmers don't like light themes? Because light attracts bugs! ... Ok, I'm sorry.
When creating this blog, I wanted a color theme change feature, so I created a small Web Component to do it.
The component turned out simple and functional, but still, something was bothering me about this piece of code:
// 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;
}
...
}
I couldn't stop thinking: "Look how many times I am manually creating HTML components and then manually setting their attributes! There has to be a cleaner way to do that."
So, I decided it was a good idea to write a simple utility function to create HTML components:
// 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;
}
With this function, I can create HTML components like this:
// theme-changer-component.js
import { a } from './createElement.js';
export default class ThemeChanger extends HTMLElement {
...
createDOMTree() {
...
// before
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';
// after
const lightThemeButton = a.button({
classList: ['light-theme-button'],
type: 'button',
title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
innerText: 'A',
});
...
}
...
}
Interesting! This code looks clean and I needed to type a little less code when compared to when I used the native DOM API.
Wow! This was a clever idea!
Now, when when I rewrote the code using this new utility, this is the result:
// 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;
}
...
}
What benefits did I get with this new approach? I can only think of two:
- I typed less code to implement the same functionality. But the amount difference is not that big anyway.
- The code may or may not look cleaner. But if we think about it, is the code from the previous approach that hard to read to justify a change? I don't think so.
But what if we go a little further? Let's nest the components creation and evaluate those 2 benefits again:
// 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;
}
...
}
The code doesn't look that clean now, and there are multiple indentation levels. And even if someone finds it easier to read, I think it depends on personal taste. The amount of code doesn't look that different too. Also, we still need to talk about what are the trade-offs of this approach:
-
Someone reading this code may have a higher cognitive load to understand what's going on, because
now there is this extra
a
object that needs to be understood instead of the known, native DOM API. - Creating utilities like this demands a high amount of automated tests to prevent the introduction of bugs on the projects that use it (we have no idea how many edge cases of HTML element creation will not work with this approach). If I make the utility function advanced enough, it is easier to just use some library that already exists, like React, instead of reinventing the wheel.
- And the biggest trade-off is that we lose almost all IDE support. When using the utility function, we lose the auto-completion feature and all tools the analyze the usage of the native DOM API's will not be able to help us. All we get from the IDE now are out of context suggestions.
Initially, the idea of a utility function to create HTML components looked pretty good. But after trying it, it proved to have more trade-offs than benefits.
We can get a simple lesson from this experiment: Don't write clever code. The tools you have at your disposal are probably doing a better job than you will ever be able to achieve. 😉