Back when I started my web development career, I abused <table> as much as any other dev out there. Before the fancy CSS flex and grid, <table> was how we got things to sit where we wanted them, in relation to other things that we got to sit where we wanted them. It was a very inelegant solution, but we needed it to work, so we found a way to make it work.
That legacy continues today, to some degree. I am now on board the flex and grid train, but that doesn’t mean <table> is left to gather dust. As my day job involves writing business apps for displaying data, the <table> is still the best data display workhorse by far when there’s a lot of data to display.
I’m also still coding in ways that will not only befuddle other developers, but also my future self. Coders understand that there are periods of almost supernatural concentration, where our intelligence disconnects from our minds and goes on some weird autopilot. We only know this has happened when we come-to, because we’re often facing a wall of code that we cannot recognize. Add to this the likelihood that if we’re unable to retrace our steps immediately after the fact, we’re blowing our own minds when we return to the code in a few days, weeks, months, or years. How does this code even work? we ask ourselves, which is often followed by a surreptitious pat on the back for being smarter than we thought we were. It doesn’t explain the code, but at least we’re finding happiness in misery.

Ground Control
Since I found styled components I’ve been approaching my projects with the idea that my forms and discreet layout objects — like tables — should be compartmentalized and reusable. This is not a new concept, of course, as “DRY” predates my life as a coder. Recently I tried to merge this approach with Storybook, a plug-in for web projects that allows for the testing of components without the need to code a UI harness within the project itself. I am sure that Storybook works well for people who have made it an integral part of their workflow, but since I am already in the groove of creating components with inline CSS and almost no external dependencies, the extra work I had to do to support Storybook wasn’t worth the cost.
The image above is my UI testing ground for some of my styled components. There’s some text inputs, a frame for sectioning off content, and a series of buttons: primary, secondary, link, and icon. At the bottom, I have the start of my new table system, which is based off my old table system, but with a bit more horsepower under the hood.
Table of Contents
Tables are weird because they have, at minimum, three parts: the table wrapper, a header, and a body. I don’t usually use footers with tables, but they’re available as well.
<table>
<thead>
<tr>
<th>Column Header 1</th>
<th>Column Header 2</th>
<th>Column Header 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Column Data A1</td>
<td>Column Data A2</td>
<td>Column Data A3</td>
</tr>
<tr>
<td>Column Data B1</td>
<td>Column Data B2</td>
<td>Column Data B3</td>
</tr>
<tr>
<td>Column Data C1</td>
<td>Column Data C2</td>
<td>Column Data C3</td>
</tr>
</tbody>
</table>
When we have data that we expect, we can set up a table so that our headers and data will display where we want them to. Unfortunately, as I want to make this flexible, being able to pre-design headers and data elements is not possible.
My approach, then, is to create a scaffolded table structure, with individual bits built from a required data structure that is passed into the custom table component.
A Custom Table Component
// 1. ----- Imports
import styled from 'styled-components';
import object from 'lodash/fp/object';
// 2. ----- Styled Component Definitions
const StyledTable = styled.table`
width: 100%;
color: var(--white);
`
const StyledThead = styled.thead`
background-color: var(--blue-grey-med);
`
const StyledTbody = styled.tbody`
`
const StyledHeaderTh = styled.th`
padding: 5px 5px;
`
const StyledBodyTr = styled.tr`
&:nth-child(odd) {
background-color: var(--blue-primary);
}
`
const StyledBodyTd = styled.td`
padding: 2px 10px;
`
// 3. ----- Dummy Definitions and Data
const dummyDef = {
Def: [
{Heading: 'One', Data: 'col1', CanSort: true, Display: true},
{Heading: 'Two', Data: 'col2', CanSort: true, Display: true},
{Heading: 'Three', Data: 'col3', CanSort: true, Display: true},
],
OnRowClick: (d) => {
alert(`The value is ${d.display}`)
},
OnRowClickObject: {display: 'col1'}
}
const dummyData = [
{col1: 'Purple', col2: 'Monkey', col3: 'Dishwasher'},
{col1: 'I', col2: 'Like', col3: 'Cheese'},
{col1: 'Tucker', col2: 'Really', col3: 'Stinks'},
]
// 4. ----- The Table Component
const Table = () =>{
return (
<StyledTable>
<StyledThead>
<tr>
{
dummyDef.Def.map(h=>{
return (
<StyledHeaderTh>
{h.Heading}
</StyledHeaderTh>
)
})
}
</tr>
</StyledThead>
<tbody>
{
dummyData.map(d=>{
return(
<StyledBodyTr
onClick={() => dummyDef.OnRowClick(
TranslateRowClickObject(
dummyDef.OnRowClickObject,
d))}
>
{
dummyDef.Def.map(h=>{
return(
<StyledBodyTd>
{d[h.Data]}
</StyledBodyTd>
)
})
}
</StyledBodyTr>
)
})
}
</tbody>
</StyledTable>
)
}
const TranslateRowClickObject = (rco,d) =>{
//We have one row of data. We need to use the values of the keys in rco
//to pull the data and build a new object to return which has the original
//rco keys with the data from d.
const _keys = object.keysIn(rco);
let _ret = {};
_keys.map(i=>{
_ret[i] = d[rco[i]]
});
return _ret;
}
export { Table }
The code above is a complete test of an initial stab at creating this kind of dynamic table.
I start with my usual imports, but I have decided to take a look at lodash. I have seen it mentioned all over the place but never looked into it because after I couldn’t really find a use for Redux in the way I code, I became wary of jumping on the popular library bandwagon. When it comes to lodash, though, I regret not having looked into it sooner, as it helps immensely with array and object manipulations, which I always end up doing, but never very well.
Section 2 are all of the styled component definitions for the table. Styled components define CSS directives beneath the umbrella of a known UI element such as <div> or, in this case, <table> and its children. We can then use the named object in place of the base element; rather than use <table> I can now use <StyledTable> and have all of the benefits of the CSS written for that element. But real talk: does this save me anything over standard CSS classes and selectors? If I write the same CSS in a central file and associate those styles with the base elements, then I don’t need to maintain all of these individual derivatives, right? Yes, but. By relying on styled components, my custom table becomes completely self-sufficient. I could easily copy and past the contents of this file into another project (which has the appropriate supporting libraries installed) and not have it butt heads with that project’s existing CSS. Right now, the only thing I am using a central CSS file for is to define a few variables that you can see in the code above, specifically colors so I can give them friendly names and don’t have to remember the hex code values.
Section 3 is my dummy definition and dummy data objects. Normally, the definition object would be constructed one level outside of the table component definition, where the custom component is used, and the data would be sourced from an API endpoint. My endpoints all return a Data Transport Object (DTO) which carries data rows in arrays, as well as operational state information like error messages and query execution time. As such, were these two objects “real”, they would get passed into the custom table component as props.
Section 4 is the custom table component, and I’ll break it down in a bit more detail.
In My Head
<StyledThead>
<tr>
{
dummyDef.Def.map(h=>{
return (
<StyledHeaderTh>{h.Heading}</StyledHeaderTh>
)
})
}
</tr>
</StyledThead>
The <StyledHead> is a styled component based off of <thead>, or the table header. I’m only coloring the background right now so I can set it off from the body.
This custom table will only accept one row of column headers, so I’ve hard-coded the <tr> or table row elements. Inside the <tr>, however, I’m using the map function to iterate over the array of objects in the dummyDef.Def key.
The dummyDef.Def key contains an array of objects. Each object defines the Heading which should display at the top of each column, the Data which should appear in that column, and two currently unused flags: CanSort and Display.
As the map function executes, each object defined in the Def array contributes the value of Heading, which is used to display the column header.
A Complex Body
dummyData.map(d=>{
return(
<StyledBodyTr
onClick={() => dummyDef.OnRowClick(
TranslateRowClickObject(
dummyDef.OnRowClickObject,
d)
)}>
{
dummyDef.Def.map(h=>{
return(
<StyledBodyTd>
{d[h.Data]}
</StyledBodyTd>
)
})}
</StyledBodyTr>
)
})
The blog design messes up this code block, so I hope it’s clear enough to understand.
Like the header objects, we start off the body by using map on the dummyData array. Each object in this array represents one data row. Each key represents the field name or alias, and the value is the value returned from the API for that field or alias. As one data row should be represented by one <tr>, we have to loop through each record in order to build our body.
Within that map loop we have to call another map loop, this time on the dummyData.Def array. If you remember, the dummyDef.Def array objects have a key called Data. The value of this key represents the field or alias in a single record of dummyData that belongs in that column. As we loop through each data record, we then have to loop through each data value and display it in the column marked by the appropriate header.
So far there’s no real curveballs. This design should hold up for any data assuming there’s a correctly formatted header definition provided to the custom table component. I have created a similar component for work and have deployed it to production numerous times and it has yet to misbehave.
The gotcha, however, is how to handle interaction.
Poking The Bear, If The Bear Was a Row of Data
Let’s revisit dummyDef for a second:
const dummyDef = {
Def: [
{Heading: 'One', Data: 'col1', CanSort: true, Display: true},
{Heading: 'Two', Data: 'col2', CanSort: true, Display: true},
{Heading: 'Three', Data: 'col3', CanSort: true, Display: true},
],
OnRowClick: (d) => {
alert(`The value is ${d.display}`)
},
OnRowClickObject: {display: 'col1'}
}
We have our header definition in the Def key, but we also have two other keys: OnRowClick and OnRowClickObject.
We may have situations where we need our users to click on a row in the table so we can take some kind of action, like selecting that record for editing on another page. It’s fairly easy to provide a function to the custom table component that can handle this, but the problem is that each click needs to pass some kind of identifying data to that function so we know which record we need to load into the edit screen. Since our header and column data are parameterized, and even though we know what data fields/aliases to expect from our API call, we can’t ever assume which data we need to pass to which function to identify the record the user want to edit.
OnRowClick is a function, defined in-line for simplicity and testing but which would be defined outside of the custom table component in a finished implementation, that is called when a user clicks on a table row. We accept an argument d, and for our test we simply call the window.alert function to display a string.
OnRowClickObject is a mapping object which represents the argument format that the function passed via OnRowClick expects. If we were to combine the object and the function, it would look something like this:
const onRowClick = ({display}) => {
alert(`The value is ${display}`)
}
In fact, this function signature is exactly what we’d want to write in a real app, outside of the custom table component, and which we pass as the value of OnRowClick.
The only issue is that OnRowClickObject is “coded” when it comes to the value of display. “col1” is the name of the field/alias in the dummyData array objects, but in the object assigned to OnRowClickObject, this means absolutely nothing. This is why we need a translator function, complements of lodash.
Everyone is Replaceable
const TranslateRowClickObject = (rco,d) =>{
const _keys = object.keysIn(rco);
let _ret = {};
_keys.map(i=>{
_ret[i] = d[rco[i]]
});
return _ret;
}
This helper function takes two arguments: the OnRowClickObject (rco) and the data row from the current iteration of the map of dummyData (d).
We use the lodash object.keysIn function to get just the keys from the rco which, in this case, is just “display”. Then, looping through all of these keys, we use the value of the key as assigned in the rco to get the data from the data row d. We create a new rco with the original keys and the actual data from the current data row which matches the original key’s value.
Basically, we go from this:
{display: 'col1'}
to this:
{display: 'Purple'}
for the first record in dummyData.
What we end up passing to the function assigned to OnRowClick looks like this:
const onRowClick = (d) => {
// d = {display: 'Purple'}
// alert(`The value is Purple`)
alert(`The value is ${d.display}`)
}
Loose Ends
Once the table component is complete — and there’s a lot still left to do — then all I’ll have to do to use it is to create a headerDef object which contains the Def, OnRowClick, and OnRowClickObject keys and values, and pass it into the custom table component. The rest will take care of itself (assuming that the value of OnRowClick is an existing function, of course, that expects an object object in the format assigned to OnRowClickObject).
So what’s left? For starters, I need to create a sorting mechanism and display for the header. This can get pretty dicey, depending on how data is returned from the API. If the API only provides a small subset of a larger data set on account of paging or some other pre-filter, then sorting will need to happen back at the API/database. If we only want to sort on the data returned from the API, then things get a lot easier, and I can do it in real time without returning to the data-well.
One last task that I want to tackle is what to do about non-data-sourced cell contents. While the current tests are promising, I have run into numerous occasions where I need to supply different actions for a single record. I usually add buttons to the last cell in a row and wire each button’s onClick handler to different functions that accept some unique data from that record in order to do the needful. I suspect that the solution to that problem in this case is somewhere in the ballpark of how I’m handling the row click, but turned up to 11 as I’ll need to pass in a completely discreet control in a specific column, assigning a function handler and somehow ensuring that clicking the button(s) pass the correct data from the current row, before I know what or where that data will be. I also need to ensure that clicking a button in a cell in a row with an OnRowClick handler doesn’t fire the OnRowClick handler when the button is clicked, which is totally something that happens unless it’s prevented.