Getting your hands dirty and feet wet with Open Web Component Recommendations...sort of.
This a cross-post of a Feb 26, 2019 article from Medium that takes advantage of my recent decision to use Grammarly in my writing (so, small edits have been made here and there), thanks for looking again if you saw it there 🙇🏽‍♂️ and if this is your first time reading, welcome!
Welcome to “Not Another To-Do App”, an overly lengthy review of making one of the smallest applications every developer ends up writing at some point or another. If you’re here to read up on a specific technique to writing apps or have made your way from a previous installation, then likely you are in the right place and should read on! If not, it’s possible you want to start from the beginning so you too can know all of our characters’ backstories...
If you’ve made it this far, why quit now?
Some Abstractions Aren’t (Just) For Your App
Often times we find ourselves staring the benefits of a quality abstraction in the eyes. For instance, if you realize that you are baking certain functionality into every component that you make, it’s probably a good idea to create an intermediary base class, or a mixin, or module export to abstract that repetition, centralize that functionality, and generally enable you to DRY (don’t repeat yourself) up your code. Other times, the benefits of making a solid abstraction are less clear. If something is really only used once in your application, it might be hard to fully sell the benefits of an abstraction (even to yourself, especially after I spent a lot of time telling you to be lazy and separate things only when you need to in previous installations of this glorious adventure we’ve embarked on together), and in these cases it can be useful to think not only about where your application might be making use of this functionality, but where your tests might, as well. (I’ll leave the argument as to whether your tests are part of your app or not to my self-conscious desire to judge every statement in this series. Or, the comments, I still love comments!)
As a refresher, let’s take a look at some testing code that we’ve already spent some time with:
it('adds a to do in response to a `todo-new` event', async () => {
const newTodo = 'New To Do';
const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.shadowRoot.querySelectorAll('to-do').length)
.to.equal(0);
el.dispatchEvent(new CustomEvent('todo-new', {
detail: newTodo,
bubbles: true,
composed: true
}));
await nextFrame();
expect(el.todos.length).to.equal(1);
expect(el.todos[0]).to.equal(newTodo);
expect(el.shadowRoot.querySelectorAll('to-do').length)
.to.equal(1);
expect(el.shadowRoot.querySelectorAll('to-do')[0].textContent)
.to.equal(newTodo);
});
In isolation, not that crazy; create a fixture, do a test, create/dispatch a Custom Event, wait a bit, do some more tests. Now, to add context, let’s look at some code from src/to-do-write.js
:
newToDo() {
if (!this.todo) return;
this.dispatchEvent(new CustomEvent('todo-new', {
detail: this.todo,
bubbles: true,
composed: true
}));
this.todo = '';
}
In isolation, also not crazy: check to see if a to-do has been supplied, create/dispatch a Custom Event, clean up the to do. Even side by side you could easily rely on the rule of three as an excuse for not thinking too hard about this. There’s, of course, more...context. Take a look at the listener in src/open-wc-app.js
:
this.addEventListener(
'todo-new',
(e) => this.addToDo(e.detail)
);
this.addEventListener(
'todo-complete',
(e) => this.completeToDo(e.detail)
);
No, this is not a third instance of the code above, but an eagle eye might have spotted the magic string that now resides across all three pieces of code. Tangentially in the land of context, you may also have noticed this code in src/to-do.js
:
completeToDo() {
this.dispatchEvent(new CustomEvent('todo-complete', {
detail: this.todoId,
bubbles: true,
composed: true
}));
}
As well as this somewhat matching test in test/to-do.test.js
:
it('removes a to do in response to a `todo-complete` event', async () => {
const completeToDo = 'New To Do';
const el = await fixture(
html`<open-wc-app
todos='["${completeToDo}"]'
></open-wc-app>`
);
expect(el.shadowRoot.querySelectorAll('to-do').length))
.to.equal(1);
expect(el.shadowRoot.querySelectorAll('to-do')[0])
.to.equal(completeToDo);
expect(el.todos[0]).to.equal(completeToDo);
el.dispatchEvent(new CustomEvent('todo-complete', {
detail: completeToDo,
bubbles: true,
composed: true
}));
await nextFrame();
expect(el.shadowRoot.querySelectorAll('to-do').length))
.to.equal(0);
});
So far we’ve seen the emergence of some magic strings that might have been acceptable in the isolation of the application code. However, when they are placed next to their associated tests, and the somewhat magic Custom Events that the strings are found in, our “abstraction needed” bells should be going off. You, your application, and its tests are a little family, and while it certainly took me longer to realize that than it should have, abstractions aren’t for any one part of the family alone! Take a look at how we can abstract some of this away via src/to-do-events.js
:
const eventOptions = {
bubbles: true,
composed: true,
}
export const toDoEventNames = {
NEW: 'todo-new',
COMPLETE: 'todo-complete',
}
const toDoEvent = (todo, event) => {
return new CustomEvent(event, {
...eventOptions,
detail: todo
});
}
export const eventCompleteToDo = (todo) => {
return toDoEvent(todo, toDoEventNames.COMPLETE);
}
export const eventNewToDo = (todo) => {
return toDoEvent(todo, toDoEventNames.NEW);
}
Now we have those pesky magic strings enumerated via toDoEventNames.COMPLETE
, and toDoEventNames.NEW
, and our Custom Event creation is sharing most of the operable parts of the process while exposing a helper to for each event to leverage that code. That means a good amount of complexity can be removed from the samples above and we get code like:
newToDo() {
if (!this.todo) return;
this.dispatchEvent(eventNewToDo(this.todo));
this.todo = '';
}
And:
completeToDo() {
this.dispatchEvent(eventCompleteToDo(this.todoId));
}
Bubbling up to:
this.addEventListener(
toDoEventNames.NEW,
(e) => this.addToDo(e.detail)
);
this.addEventListener(
toDoEventNames.COMPLETE,
(e) => this.completeToDo(e.detail)
);
Along the way we’ve also reduced maintenance costs, in the case we need to refactor, and future development cost, in the case we add new Custom Events for extended data interactions in the future.
Speaking of data and managing it...you might be interested in checking out the next and last entry in our long-running telenovela.
The Short Game
As voted on by a plurality of people with opinions on such topics that are both forced to see my tweets in their Twitter feed and had a free minute this last week, a 9000+ word article is a no, no.
So, it is with the deepest reverence to you my dear reader that I’ve broken the upcoming conversations into a measly ten sections. Congratulations, you’re nearing the end of the first! If you’ve enjoyed yourself so far, or are one of those people that give a new sitcom a couple of episodes to hit its stride, here’s a list of the others for you to put on your Netflix queue:
- Not Another To-Do App
- Getting Started
- Test Early, Test Often
- Measure Twice, Lint Once
- Make it a Component (The intro/outro relationship on these articles could use some componentization...just apply bad joke via the Light DOM.)
- Make it a Reusable Part
- Does Your Component Really Need to Know That?
- Separate Things Early, Often, and Only as Needed
- Some Abstractions Aren’t (Just) For Your App (you are here)
- Reusable and Scaleable Data Management/And, in the end... (Coming Soon to dev.to)
- See the app in action
Special thanks to the team at Open Web Components for the great set of tools and recommendations that they’ve been putting together to support the ever-growing community of engineers and companies bringing high-quality web components into the industry. Visit them on GitHub and create an issue, submit a PR, or fork a repo to get in on the action!
Top comments (0)