This Is How To Make A Really Good Users Table

by kleamerkuri
Users table vanilla JS JSON data.

Have you ever come across a job application that presents a project challenge?

I recently stumbled upon one that caught my attention. It involved developing a one-page web application that receives and renders JSON data into the DOM.

The requirement is to use the following API: https://jsonplaceholder.typicode.com/

That API should provide all the endpoints we’ll need for users, posts, etc.

We’re asked to use jQuery or Vanilla JS to display each user in a table. When a user on the table is selected, all posts created by that user will display.

There are no further limitations in how we approach this as long as we meet the objectives listed above.

So, I thought, why not show you my approach to a coding project like this?

👋 Hey! Find a live demo on CodePen!

Throughout this tutorial, I’ll be your guide, providing insights and practical tips as we tackle this project together.

I’ll use Vanilla JS throughout the project, fetching data using the Fetch API and displaying the content in a table with a posts list.

Now, let’s roll up our sleeves and get ready to bring this idea to life!

Who’s excited? 🙋‍♀️

Breaking down the project

Before diving into code, start by breaking down the project asks.

Since we’re working with data, it’s good practice to understand a few things such as:

  1. The data structure of the API we’ll be using
  2. The data we need for display
  3. The endpoints that provide the necessary data

Only with that information can we confidently march onto building the application seamlessly and efficiently 😌

My process for tackling the three points above can be broken down into three steps:

1) Figuring out the API

Reference the documentation of the API to understand the existing endpoints and available data.

Depending on the readability of the documentation, I must admit that this step can sometimes take the longest.

If the API documentation is too sparse, you’re left scratching your head. But if it’s too nuanced, you’ll be jumping from page to page clueless.

Luckily, the project API is a simple one that gets straight to the point. Sorry for scaring ya (’tis real-life situations I shared) 😬

2) Understanding API data

Use Postman, or any other similar platform, to analyze the data structure of the API.

Ask yourself these questions:

Is the response a list of objects?

How many objects does a single response yield?

How many properties does each object hold? What are their structures?

Understanding the API data structure helps us figure out if the response of a single endpoint provides the data we need.

If a single data object from a response doesn’t hold all the properties we need, then we’ll supplement partial data with that found in another endpoint.

3) Identifying display data

Based on project requirements, identify the API endpoints necessary to get the data.

We’re interested in the /posts and /users endpoints.

For each USER, we display:

  • full name
  • username
  • status
    • active or inactive
    • determined by checking if a USER’s ID is found in the response of the POSTs
  • location
  • phone
  • contact
    • a dummy button, potentially holding email and website info
  • number of posts

For each POST, we display:

  • title
  • views and publish date
    • both random since the data isn’t available on the API

Setting up the HTML

In this section, we’ll set up the HTML structure required for displaying the user table with each user’s corresponding posts.

Start by creating a container element that will hold the user table along with a title. I call mine “main”.

Inside the container, create an empty <table> element with appropriate table headings to represent the user and post data.

<div id="main">
  <div class="table_title">
    <h2>Users</h2>
  </div>
  <div class="table_body">
    <table>
      <thead>
        <tr>
          <th>User</th>
          <th>Status</th> 
          <th>Location</th>
          <th>Phone</th>
          <th>Contact</th>
          <th>Posts</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </div>
</div>


Read: How To Create Simple HTML Tables (with examples)

We’ll populate the table body with dynamic rows of data later with JavaScript.

You should now have:

Setting HTML structure of user table.

Populating the user table using JavaScript

Using Vanilla JavaScript and the Fetch API, we can retrieve the user and post data from the two endpoints.

Let’s begin by defining variables and functions.

usersData and postsData are the two fetch actions that retrieve data from the API endpoints.

Read: How To Use JavaScript Fetch API With Pokémon Example

const table = document.querySelector('tbody'),
      url = "<https://jsonplaceholder.typicode.com/>";

const usersData = async () => {
  let resp = await fetch(url + 'users/');
  let data = resp.json();
  return data;
};
const postsData = async () => {
  let resp = await fetch(url + 'posts/');
  let data = resp.json();
  return data;
};


setStyle is a helper function whose purpose is to make style application on JS-constructed elements easier.

// dynamic style class setting
const setStyle = (styleList, elm) => {
  styleList.forEach( style => {
    elm.classList.add(style);
  });
}


userPosts gets all the posts that belong to a single user based on the user’s ID. The return value includes the number of posts as count.

showPosts is responsible for displaying a user’s posts along with the details of each post like publish date and views.

I’m, also, using Bootstrap Icons for the calendar and views icons to complete the look.

// get all posts for a single user
const userPosts = (userID, posts) => {
  let userPosts = posts.filter( post => post.userId === userID);
  
  return {
    posts: userPosts,
    count: userPosts.length
  }
};
// display user's posts
const showPosts = (postList) => {
  let div = document.createElement('div'),
      ul = document.createElement('ul');
  
  setStyle(['post_list'], ul);
  
  // selection of choices for publish date
  const getRandomMonth = () => {
    const month = ["January","February","March","April","May","June","July","August","September","October","November","December"];
    
    return month[Math.floor(Math.random() * (month.length))].slice(0, 3);
  }
  // random selection inclusive of max
  const getRandomNum = (max, min=0) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
  
  postList.forEach( (item, i) => {
     let li = document.createElement('li'), 
        name = document.createElement('h3'),
        items = document.createElement('div'),
        pubDate = document.createElement('span'), 
        views = document.createElement('span');

    // capitalize first letter of post title
    let title = postList[i].title.charAt(0).toUpperCase() + postList[i].title.slice(1);
    name.textContent = title;
    
    pubDate.innerHTML = '<i class="bi bi-calendar4"></i> ' + getRandomMonth() + ' ' + getRandomNum(2023, 2001);
    
    views.innerHTML = '<i class="bi bi-eye"></i> ' + getRandomNum(10000).toLocaleString();
    
    [pubDate, views].forEach( item => items.appendChild(item));
    
    [name, items].forEach( item => li.appendChild(item));
    
    ul.appendChild(li);
  });
   
  div.appendChild(ul);
  
  return div.innerHTML;
};


Note: The publish date and view count is totally made-up since the data isn’t available via the API. Hence all the randomization 😉

getRandomMonth returns a random item from an array. In this case, the random item is a month, shortened to the first three letters using the slice() method.

Similarly, getRandomNum returns a random based on a provided max and min. Then use the toLocaleString() method to format a number as a string meaning it has the commas where they should be.

Practice working with dates 👉 How To Build An Amazing Custom Datepicker

Now let’s define some get-functions that quite literally “get” the data cells that display on the table.

const getUsername = (n, u) => {
  const div = document.createElement('div'),
        name = document.createElement('p'),
        user = document.createElement('span');
  
  name.innerText = n;
  name.classList.add('user_name');
  user.innerText = '@' + u;
  user.classList.add('user_username');
  
  [name, user].forEach( item => div.appendChild(item));
  return div.innerHTML;
};
const getStatus = (isActive) => {
  const div = document.createElement('div'),
    indicator = document.createElement('span'),
        indicatorText = document.createElement('span');
  
  let classList = [
    'user_status',
    isActive ? 'green' : 'red'
  ]

  setStyle(classList, indicator);
  
  indicatorText.innerText = (isActive ? 'Active' : 'Inactive');
  
  div.innerHTML = indicator.outerHTML + indicatorText.outerHTML;
 
  return div.innerHTML;
};
const getPhone = (phone) => {
  let number;
  // add space after closing parens
  phone = phone.replace(/\\)/, ') ');
   
  // remove entensions
  if(phone.includes('x')) {
    number = phone.split('x')[0].trim();
    return number
  } else {
    return phone
  }
}
const getContact = () => {
  const div = document.createElement('div'),
        button = document.createElement('button');
  
  let classList = [
    'btn_pill',
    'contact_btn'
  ];
  
  setStyle(classList, button);
  
  button.innerText = 'Contact';
  button.setAttribute('type', 'button');
  
  div.appendChild(button);
  return div.innerHTML;
};


Then use these get-functions in a function that returns a table row—I call this, in all my creative genius, addRow().

const addRow = (user, isActive, posts) => {
  let tr = document.createElement('tr'),
      tdUser = document.createElement('td'),
      tdStatus = document.createElement('td'),
      tdLocation = document.createElement('td'),
      tdPhone = document.createElement('td'),
      tdContact = document.createElement('td'),
      tdPosts = document.createElement('td'),
      tdPostList = document.createElement('td'),
      trPostList = document.createElement('tr');
  
  let { 
    id,
    name, 
    username,
    address: { city },
    phone
  } = user;
  // console.log(name, username)
  
  let { count, posts: postsList } = userPosts(id, posts);
  
  tdUser.innerHTML = getUsername(name, username);
  tdStatus.innerHTML = getStatus(isActive);
  tdLocation.innerText = city;
  tdPhone.innerText = getPhone(phone);
  tdContact.innerHTML = getContact();
  tdPosts.innerHTML = `
  <strong>${count}</strong> <button id="posts-btn-${id}" class="post_total btn_pill" type="button" aria-controls="post-list-${id}"><span>View all <span class="arrow_icon"><i class="bi bi-chevron-down"></i></span></span></button>
  `;
  tdPostList.innerHTML = showPosts(postsList); 
  tdPostList.setAttribute('colspan', '100%');
  
  [tdUser,tdStatus,tdLocation, tdPhone, tdContact,tdPosts].forEach( item => tr.appendChild(item));
  
  trPostList.appendChild(tdPostList);
  trPostList.setAttribute('id', `post-list-${id}`);
  trPostList.setAttribute('aria-labelledby', `posts-btn-${id}`);
  trPostList.setAttribute('aria-hidden', true);
  trPostList.classList.add('hidden');
  
  table.appendChild(tr);
  table.appendChild(trPostList);
}


Since we pre-defined a lot of the individual actions to populate each cell of the row, all that’s left to do in addRow() is to execute those functions.

Follow by adding each user to a row like so:

( async () => {
  const users = await usersData(); // 10 users
  const posts = await postsData(); // 100 posts
  
  let isActive;
  // add each user to table 
  users.forEach( user => {
    isActive = posts.some( post => post.userId === user.id);
    addRow(user, isActive, posts);
  });
})();


Determine the isActive state by using the some() array method to check if there’s at least one post in the post list that is associated with a user’s ID.

Adding table rows with data cells using Javascript.

Adding CSS using SCSS

We’ve come a long way in terms of displaying a completed, albeit bare, users table.

To make the users table visually appealing with a modern web finish (per our design inspos), we’ll use SCSS to add custom styles.

Get started with SCSS 👉 How To Make A Beautiful Accessible Accordion With SCSS

Begin by creating a new SCSS file and importing the mapped CSS into your HTML document.

Start styling the table wrapper and table itself along with the table headings.

body {
  margin: 0;
  font-family: Roboto, Helvetica, sans-serif;

  & > * {
    box-sizing: border-box;
  }

  #main {
    margin: 10px auto;
    // background: yellow;
    max-width: 800px;
    width: 100%;
    color: #232627;

    .table_title {
      margin: 40px 20px 10px 20px;
    }

    .table_body {
      margin: 5px;
      width: 850px;

      table {
        width: 100%;
        margin: 0 auto;
        text-align: left;
        table-layout: auto;
        border-collapse: collapse;
     
        tr {
          th,
          td {
            padding: 10px;
          }
          
          &.hidden {
            visibility: collapse;
          }
          
          &[aria-hidden=false] {
            border-top: 2px solid #C5D8C6;
            border-bottom: 2px solid #C5D8C6;
            
            li:hover {
              cursor: pointer;
              background: #D3E5EF;
            }
          }

          th {
            font-family: Ariel, sans-serif;
            font-size: 0.85rem;
          }
        }
      }
    }
  }
}


In the code above, we collapse two things: the visibility of the rows with a hidden class and the table borders.

I collapse visibility to hide the extra space when hiding the table rows since visibility doesn’t remove an element from the flow of the page (unlike setting the display).

Whereas I collapse the table borders to add a border on each table row element.

Read: How To Build An Interactive Menu Component

Note 👀
The hidden class and the aria-hidden CSS selectors are conditionally applied in the JS code. They depend on the display state of the posts list.

Styling the table wrapper and table headings.

Now let’s add styles to the table cells. The neat thing with SCSS is that our styles remain organized within their respective cells!

body {
  ...

          td {
            &:not(:first-of-type) {
              color: #606064;
            }
            .btn_pill {
              border: none;
              background: none;
              border-radius: 15px;
            }
            .user_ {
              &name {
                // line-height: 0;
                font-size: 1.02rem;
                font-weight: 600;
                color: #4b4b4b;
                margin: 5px 10px 5px 0;
              }

              &username {
                // line-height: 0.5rem;
                color: #a1a1a1;
                font-size: 0.96rem;
              }

              &status {
                display: inline-block;
                width: 10px;
                height: 10px;
                border-radius: 5px;
                margin-right: 5px;

                &.green {
                  background: #309344;
                }

                &.red {
                  background: #ca3343;
                }
              }
            }
            .contact_btn {
              background: linear-gradient(
                to right,
                rgb(36, 74, 210) 50%,
                #0379f9 50%
              );
              background-size: 200%;
              background-position: right;
              padding: 5px;
              color: white;
              width: 70px;
              transition: background 0.2s ease;

              &:hover {
                cursor: pointer;
                background-position: left;
              }
            }
            .post_total {
              background: #00b092;
              color: white;
              padding: 4px;
              margin-left: 5px;
              width: 80px;

              &:hover {
                cursor: pointer;
              }

              & > * {
                pointer-events: none;
                display: inline-block;
              }

              & > span {
                font-size: 0.75rem;
              }

              .arrow_icon {
                // font-size: 1.005rem;
                position: relative;
                top: 1px;

                &.arrow_up {
                  display: inline-block;
                  transform: scaleY(-1);
                  top: -1px;
                }
              }
            }
            .post_list {
              list-style-type: none;
              margin: 0;
              padding: 0;
              
              li {
                // margin-bottom: 25px;
                padding: 2px 5px 25px;
                
                h3 {
                  margin-bottom: 8px;
                  font-weight: 500;
                }

                div {
                  span {
                    display: inline-block;
                    font-size: .85rem;
                    color: #909DB5;
                    
                    .bi {
                      font-size: .9rem;
                      margin-right: 2px;
                      
                      &-eye {
                        font-size: 1.05rem;
                        position: relative;
                        top: 1px;
                      }
                    }

                  &:nth-of-type(2) {
                    margin-left: 20px;
                  }
                }
                }
              }
            }
          }
        }
      }
    }
  }
}

Stylilng the table cells.

Most of the cell customizations lie within this td style block.

For example, take the sliding background of the dummy contact button when hovering over it.

I use a combination of linear-gradient and background properties to create the illusion of a color slider that transitions the button from the resting color shade to a darker one.

The background transition adds a nice interactive feature that also serves as an indicator of the available action.

Then there’s the case of setting pointer-events for each child element in each total button to trigger a parent click event when child elements are clicked.

Toggling the display of the post list

We currently have a rather dashing users table with each row displaying information about a particular user.

At the moment, however, we’re unable to see the post list for a particular user on the table.

That’s because we’ll be working with the display of panels to toggle the appropriate classes based on a logical setup.

To show the post list, listen for a click event on each of the post_total buttons.

Once a button is clicked, the togglePanel() function executes.

document.querySelectorAll('.post_total').forEach( btn => {
    const togglePanel = e => {
      let target = e.target.getAttribute('aria-controls');
      let postList = table.querySelector(`#${target}`);
      let isHidden = postList.classList.contains('hidden');
      
      postList.classList.toggle('hidden');
      postList.setAttribute('aria-hidden', !isHidden);
      e.target.querySelector('.arrow_icon').classList.toggle('arrow_up');
    };

    btn.addEventListener('click', togglePanel);
  });

Complete users table UI.

And for a little on-screen action:

It’s a wrap

Done! We just created a user table that shows a list of users and the posts of each user.

This is a project I’d be proud to submit for consideration on a job application 🙌

We outlined the plan for organizing data and presenting it prior to starting (a great tip for any developer).

The HTML structure provides the foundation, the SCSS styles enhance the visual appearance, and the Vanilla JavaScript logic uses the Fetch API and JSON data to populate the table dynamically with user and post information.

You can find all the source code on GitHub.

See ya next time!

Related Posts