Table of Contents
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:
- The data structure of the API we’ll be using
- The data we need for display
- 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:
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 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 👀
Thehidden
class and thearia-hidden
CSS selectors are conditionally applied in the JS code. They depend on the display state of the posts list.
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;
}
}
}
}
}
}
}
}
}
}
}
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);
});
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!