Let's make a table component with JavaScript

gohomewho

Gohomewho

Posted on November 19, 2022

Let's make a table component with JavaScript

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.
table demo screenshot from MDN

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
}
Enter fullscreen mode Exit fullscreen mode

Grab the dummy data from https://jsonplaceholder.typicode.com/users and store it in a variable.

const users = [ 
  //...
]
Enter fullscreen mode Exit fullscreen mode

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',
]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Our result.
the result at this point

We can easily change the columns we want to display.

const nameOfDefaultColumns = [
  'id',
  'name',
  'username',
  'email',
  'phone',
  'website',
  'company' // add this
]
Enter fullscreen mode Exit fullscreen mode

The column appears! But the data is definitely not correct.
company data display [object object]

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]'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Display all columns with processed values.
screenshot

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
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
gohomewho
Gohomewho

Posted on November 19, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related