Let's make a table component with JavaScript
Tags: webdev, javascript, beginners, tutorial
Published: 2022-11-19 09:40:42

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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table) is structured. 
![table demo screenshot from MDN](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryv0epgdczt0d2un1otf.png)

Let's write the overall structure. 
```js 
// 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.
```js
const users = [ 
  //...
]
``` 

Define the default columns we want to display. we'll make the columns can be toggled on and off in later series.
```js
const nameOfDefaultColumns = [
  'id',
  'name',
  'username',
  'email',
  'phone',
  'website',
]
```

Add a parameter `columns` to `createTableHead` so it can make each `th` element according to `columns`.
```js
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.
```js
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.
```js
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
}
```

Our result.
![the result at this point](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3l22l72wpoykpjo1hk7.png)

We can easily change the columns we want to display. 
```js
const nameOfDefaultColumns = [
  'id',
  'name',
  'username',
  'email',
  'phone',
  'website',
  'company' // add this
]
```

The column appears! But the data is definitely not correct.
![company data display [object object]](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ii92983jvastorjvfg2.png)

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.

```js
// 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.


```js
// 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.

```js
// 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.
```js
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.
![screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/df89lqd28hs375c9ryb9.png)

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.
```js
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
}
```