Adding and removing items from a shopping list is a very common UI pattern, one often accomplished with a framework (or two, or three…). This week I realised that the pattern could be achieved using CSS and vanilla JavaScript…
The Base
The fundamental structure of the table is presented in HTML, and is rendered without any UI elements; that way, if the JavaScript fails, or is somehow blocked, the user will still see the list; they just won’t be able to edit it.
<table id="zen">
<caption>AMD Zen Processors</caption>
<thead>
<tr><th>Name<th>Speed<th>Cores<th>Threads<th>Price
<tbody>
<tr><td>SR7<td>3.5Ghz<td>8<td>16<td>$499
<tr><td>SR7<td>3.2GHz<td>8<td>16<td>$349
<tr><td>SR5<td>3GHz<td>6<td>12<td>$250
</table>
The table has some associated CSS rules:
#zen {
border-collapse: collapse;
}
#zen tr td, #zen tr th {
padding: .4rem;
}
#zen tr td:nth-of-type(3), #zen tr td:nth-of-type(4) {
text-align: center;
}
#zen thead {
border-bottom: 2px solid #333;
}
#zen tbody tr {
border-bottom: 1px solid #ddd;
}
Adding the Buttons
The buttons are inserted by adding them to a table cell that is concatenated to the end of each row in the body of the table:
let procRows = zen.querySelectorAll("tbody tr");
for (let i = 0; i < procRows.length; i++) {
procRows[i].innerHTML += '<td><button title="Remove"></td>';
}
In the CSS, these buttons use a background of an SVG image for the trashcan:
#zen button {
width: 36px; height: 36px;
background-image: url(trashcan.svg);
margin-left: 1rem; border: none;
background-position: center 4px;
background-repeat: no-repeat;
background-size: 25px 25px;
opacity: .6;
transition: .5s;
}
There’s also a :hover
state to change the appearance of the button when the mouse moves over it:
tbody tr button:hover {
opacity: .9;
cursor: pointer;
}
Later the script uses event bubbling to listen for a click in the <tbody>
of the table. If this click is on the <button>
element, the script finds the cell that the button
is inside of, then finds the table row surrounding that element, and applies a class to it; it also removes the button.
zen.querySelector("tbody").addEventListener("click", function(e) {
if (e.target.nodeName == "BUTTON") {
let cell = e.target.parentNode;
cell.parentNode.classList.add("hidden");
e.target.remove();
}
})
Going Schmooop
CSS cannot animate the height
of an element; however, the height of a td
containing text content (and thus it’s containing row) is calculated from just two things:
- The text
font-size
- The
padding
on the table cell
…and we can animate both of those properties:
@keyframes hide {
to {
font-size: 0;
padding: 0;
}
}
That animation sequence is called when a class of hide
is added to the table row:
tr.hidden td {
animation: hide 1s 1s forwards;
}
tr.hidden {
opacity: 0;
border-bottom: none;
}
The animation has a 1 second execution after a 1 second delay.
Conclusion
The associated script and CSS creates a straightforward “remove” animation for a selected table row; if you wanted to fully remove the row from the DOM (rather than merely hiding it), you could remove
it after the animation completed.
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/LbOaKz