In this tutorial, we'll make a table component with JavaScript. We'll make it can be populated with any data and add more features to it in the following series.
This is how a table is structured.
Let's write the overall structure.
// Create a `table` element and add `thead` and `tbody` to it.
function createTable() {
const table = document.createElement('table')
const thead = createTableHead()
const tbody = createTableBody()
table.appendChild(thead)
table.appendChild(tbody)
return table
}
// create a thead element
function createTableHead() {
const thead = document.createElement('thead')
const tr = document.createElement('tr')
// add th of each column to tr
thead.appendChild(tr)
return thead
}
// create a tbody element
function createTableBody() {
const tbody = document.createElement('tbody')
// add tr of each row of data to tbody
return tbody
}
Grab the dummy data from https://jsonplaceholder.typicode.com/users and store it in a variable.
const users = [
//...
]
Define the default columns we want to display. we'll make the columns can be toggled on and off in later series.
const nameOfDefaultColumns = [
'id',
'name',
'username',
'email',
'phone',
'website',
]
Add a parameter columns
to createTableHead
so it can make each th
element according to columns
.
function createTableHead(columns) {
const thead = document.createElement('thead')
const tr = document.createElement('tr')
// create th element for each column
columns.forEach(name => {
const th = document.createElement('th')
th.textContent = name
tr.appendChild(th)
});
thead.appendChild(tr)
return thead
}
Add two parameter columns
and dataList
to createTableBody
so it can make tr
for each row of dataList
and columns of each row.
function createTableBody(columns, dataList) {
const tbody = document.createElement('tbody')
// create rows for each item of dataList
dataList.forEach(eachDataObject => {
const tr = document.createElement('tr')
// create cells of each column for the row
columns.forEach((columnName) => {
const td = document.createElement('td')
// display the data of that column
td.textContent = eachDataObject[columnName]
tr.appendChild(td)
})
tbody.appendChild(tr)
});
return tbody
}
Note that we use columns.forEach()
to create column headers in createTableHead
and create each column data of each row in createTableBody
. This guarantees that the data of each column would match.
This is the code at this point.
const user = [ /* https://jsonplaceholder.typicode.com/users */ ]
const nameOfDefaultColumns = [
'id',
'name',
'username',
'email',
'phone',
'website',
]
const table = createTable(nameOfDefaultColumns, users)
document.body.appendChild(table)
function createTable(columns, dataList) {
const table = document.createElement('table')
const thead = createTableHead(columns)
const tbody = createTableBody(columns, dataList)
table.appendChild(thead)
table.appendChild(tbody)
return table
}
function createTableHead(columns) {
const thead = document.createElement('thead')
const tr = document.createElement('tr')
columns.forEach(columnName => {
const th = document.createElement('th')
th.textContent = columnName
tr.appendChild(th)
});
thead.appendChild(tr)
return thead
}
function createTableBody(columns, dataList) {
const tbody = document.createElement('tbody')
dataList.forEach(eachDataObject => {
const tr = document.createElement('tr')
columns.forEach((columnName) => {
const td = document.createElement('td')
td.textContent = eachDataObject[columnName]
tr.appendChild(td)
})
tbody.appendChild(tr)
});
return tbody
}
We can easily change the columns we want to display.
const nameOfDefaultColumns = [
'id',
'name',
'username',
'email',
'phone',
'website',
'company' // add this
]
The column appears! But the data is definitely not correct.
Because the data was converted to string before assigning to td.textContent
, the toString()
was called implicitly. When we call toString()
on an object, it will return '[object Object]'
, This is also the reason why sometimes we try to console.log an object but we see '[object Object]'
, because it is implicitly converted to a string.
// a number
const id = 1
id.toString()
// '1'
// an object
const obj = {}
obj.toString()
// '[object Object]'
This means we need to process the data before assigning to td.textContent
. This also means that if we want to display something more complex, we can't use td.textContent
because it will only display string.
We need a way to process the data inside createTableBody
. But we can't directly process the data because the data from dataList
is dynamic. So how do we process the data inside createTableBody
but in the meantime control the actual process from outside? It is like how we pass data to createTableBody
. This time we want to pass a formatter.
// add new parameter `columnFormatter`
function createTableBody(columns, dataList, columnFormatter) {
const tbody = document.createElement('tbody')
dataList.forEach(eachDataObject => {
const tr = document.createElement('tr')
columns.forEach((columnName) => {
const td = document.createElement('td')
const columnValue = eachDataObject[columnName]
// if we have a custom formatter for this column
// we want to use it
if (columnFormatter && columnFormatter[columnName]) {
const formatterOfColumn = columnFormatter[columnName]
if (formatterOfColumn) {
const formatted = formatterOfColumn(columnValue)
// `appendChild` only accept node (element)
// `append` accept both string and node (element)
td.append(formatted)
}
}
else {
// otherwise we simply display the "string version" value
td.textContent = columnValue
}
tr.appendChild(td)
})
tbody.appendChild(tr)
});
return tbody
}
We call createTableBody
from createTable
, so we also need to add the columnFormatter
parameter to createTable
in order to pass it.
// add columnFormatter parameter
function createTable(columns, dataList, columnFormatter) {
const table = document.createElement('table')
const thead = createTableHead(columns)
// call createTableBody with columnFormatter
const tbody = createTableBody(columns, dataList, columnFormatter)
table.appendChild(thead)
table.appendChild(tbody)
return table
}
Define column formatter. An object that each key is the column name and maps to a function that process the column value.
const columnFormatter = {
'company': (data) => {
const p = document.createElement('p')
const strong = document.createElement('strong')
strong.textContent = ` (${data.catchPhrase})`
p.append(data.name, strong)
return p
},
'address': (data) => {
const {
street,
suite,
city,
zipcode,
} = data
// I don't know how people format address in US.
// Sorry if this is incorrect
return `${street} ${suite} ${city} ${zipcode}`
}
}
const table = createTable(nameOfDefaultColumns, users, columnFormatter)
document.body.appendChild(table)
Display all columns with processed values.
This is great! We've already done what we need. It should be able to populate any data. There is one last thing I want to do before closing. Let's refactor the code a little bit.
Move the logic that makes table row from createTableBody
to a new function createTableRow
so the code can be more readable.
function createTableBody(columns, dataList, columnFormatter) {
const tbody = document.createElement('tbody')
dataList.forEach(eachDataObject => {
const tr = createTableRow(columns, eachDataObject, columnFormatter)
tbody.appendChild(tr)
});
return tbody
}
function createTableRow(columns, dataOfTheRow, columnFormatter) {
const tr = document.createElement('tr')
columns.forEach((columnName) => {
const td = document.createElement('td')
const columnValue = dataOfTheRow[columnName]
const formatterOfColumn = columnFormatter && columnFormatter[columnName]
if (formatterOfColumn) {
const formatted = formatterOfColumn(columnValue)
td.append(formatted)
}
else {
td.textContent = columnValue
}
tr.appendChild(td)
})
return tr
}
Top comments (2)
❤️👍👍
Excellent article