Skip to content

A simple responsive single page that takes data from the Random User Generator API and then builds a fully accessible HTML table. The column can sort when the user clicks the column header button with a mouse or uses the enter/space key.

License

Notifications You must be signed in to change notification settings

abuna1985/sortable-table

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 

Repository files navigation

made-with-javascript GitHub commits GitHub watchers GitHub license

Logo

Sortable Columns HTML Table

View Demo

Table Of Contents

About The Project

Desktop view of the sortable columns table

This project is a simple single-page responsive design which takes data from the Random User Generator API and builds a table which can sorted by the column headers with a mouse and/or keyboard.

This project is a simple responsive web page which takes data from the Random User Generator API and builds an accessible table which can sorted by the column headers with a click of a mouse and/or enter/space key.

Visual Examples

Click here to see what happens when the columns are clicked on sortable table click demo gif

Click here to see what happens when the tab key and shift+tab key is pressed sortable table tab/enter demo gif

Click here to see the Web Accessibility Evaluation Tool (WAVE) report summary Wave Report: 11 Features, 26 Structural Elements, 132 ARIA labels

Click here to see the mobile view of the page

Mobile View

mobile view of the sorted column table

Random User API

Read the API documentation to find out more about the response values and how to test the API. Notice the URL I am using for this project is requesting 10 users (results=10) from the United States (nat=us): https://randomuser.me/api/?nat=us&results=10

Click Here to see the random user properties available
  • gender: (string) gender (male/female),
  • name: (object) contains name data
    • title: (string) title (Mr., Ms, etc)
    • first: (string) first name
    • last: (string) last name
  • location: (object) contains location data
    • street: (string) street number and name
    • city: (string)city
    • state: (string) state
    • postcode: (string) zip/postal code
  • coordinates: (object) contains coordinates data
    • latitude: (string) latitude
    • longitude: (string) longitude
  • timezone: (object) contains time zone data
    • offset: (string) timezone offset
    • description: (string) time zone
  • email: (string) email address
  • login: (object) contains login data
    • uuid: (string) unique user id,
    • username: (string) username
    • password: (string) password
    • salt: (string) salt hash
    • md5: (string) md5 hash
    • sha1: (string) sha1 hash
    • sha256: (string) sha256 hash
  • dob: (object) contains age data
    • date: (timestamp) date of birth
    • age: (number) age of person
  • registered: (object) contains registration data
    • date: (timestamp) registration
    • age: (number) age of membership
  • phone: (string) phone number
  • cell: (string) cell phone number
  • id: (object) contains id data
    • name: (string) id name
    • value: (string) id value
  • picture: (object) contains picture data
    • large: (string) URL of large image
    • medium: (string) URL of medium image
    • thumbnail: (string) URL of thumbnail image
  • nat: (string) nationality

Back to Top

Requirements

  1. Use the result a from the Random User Generator API
  2. Use HTML, CSS and Javascript to show the data in a readable table (including mobile view)
  3. All columns should have the ability to be sorted by mouse and/or keyboard

As a user, I should:

  • See a loading state when the page initially renders
  • See an HTML table when the data is successfully loaded
  • See all the HTML table data in mobile and tablet view
  • See an error message within the table body if it is not working
  • Be able to click on a column, see a visual cue that the column has been selected
  • Be able to use the following keyboard keys to control:
    • Direction Keys
      • tab, shift+tab
      • , , ,
      • w, s, a, d
      • home, end
    • Sorting
      • enter, space

Bonus Requirements

As a developer, I should

  • Implement 2 examples of caching in order to increase the overall performance of the page
  • Use the BEM (Block Element Modifier) naming convention for CSS class names
  • Use accessibility principles to ensure the page is accessible by the browser and any assistive technologies connected as well (i.e screen readers)

Back to Top

My Process

Since the requirements were fetching API data and rendering a table, I approached it the following way:

1. HTML

Create an index.html file and fill it with the elements that were not going to be changing like the <header> and root <table> element. Add BEM (Block Element Modifier) naming convention for adding class names (i.e c-header and c-table).

2. JavaScript

Create a script.js and write out the following functions:

  • fetch data from the Random User API
    • render the contents of the <table> element (<th>, <tr>, <td>, etc.) with the Random User API data
  • Create Event Listeners:
    • Click
      • <button> in Column header (<th>)
      • when clicked, it sorts the table ascending/descending order and rerenders the page with the results
    • Keydown
      • left arrow, up arrow, a, w
        • Move the focus to the previous HTML element with a tabindex attribute
      • right arrow, down arrow, d, s
        • Move the focus to the next HTML element with a tabindex attribute
      • home
        • Move the focus to the first HTML element with a tabindex attribute
      • end
        • Move the focus to the last HTML element with a tabindex attribute

3. CSS

  1. Add Normalize.css to reset the CSS browser defaults
  2. Create a style.css and add styles to:
  • Header - .c-header
    • Header Title - .c-header__title
    • Header Subtitle - .c-header__subtitle
  • Table - c-table
    • table header - c-table__head
      • header cell (th) - .c-table__th
        • button - .c-table__button
    • table body - c-table__body
      • table row (tr) .c-table__tr
        • table data (td) .c-table__td
  • Loading Screen - .l-loading-container .is-loading
  • Animations
    • move keyframe animation
    • grow keyframe animation
  • Mobile view styling @media screen and (max-width: 768px)

Back to Top

Built with

  • Semantic HTML5
  • CSS3
    • Normalize.css
    • CSS Animation (loading screen)
    • CSS custom properties
    • BEM naming convention
  • ES6 JavaScript
    • Async/Await
    • Fetch
    • Closures/Memoization

Back to Top

What I Learned

Web Accessability

Semantic HTML & ARIA attributes

After reviewing the Deque University Sortable Table Example, it looks like making a <table> with the appropriate nested table elements (<thead>, <tbody> <th>, <tr>,<td>).

Here is a skeleton example with the recommended ARIA attributes:

<table role="grid" aria-readonly="true">
  <thead>
    <tr role="row">
      <th role="columnheader" scope="col">col 1<th>
      <th role="columnheader" scope="col">col 2<th>
      <th role="columnheader" scope="col">col 3<th>
    </tr>
  </thead>
  <tbody>
    <tr role="row">
      <th scope="row" role="rowheader">data 1</th>
      <td role="gridcell">data 2</td>
      <td role="gridcell">data 3</td>
    <tr>
  </tbody>
</table>

For assistive technology, It is preferred that the selected <th> have the following attribute to let the reader know which order the column is sorted:

  • aria-sort="ascending"
  • aria-sort="descending"

NOTE: some ARIA attributes may be built into the semantic table elements. I found conflicting information and decided to add the ARIA attributes to ensure they are available to any assistive technologies.

Back to Top

Performance Optimizations

Caching API Data in SessionStorage

In most API fetching demos, the API call is made as the page is rendered. I decided to use SessionStorage to store the initial API call data. After the initial fetch, the table will pull the data directly from SessionStorage.

Once the user closes out the window tab, the data from session storage is removed. Below is the snippet where I added session storage logic:

tableBody.innerHTML = displayLoadingContainer();
if (sessionStorage.getItem('userdata')) {
// Use the data from session storage
results = JSON.parse(sessionStorage.getItem('userdata'));
// console.log('session storage used');
// console.log('--------------------');
} else {
// fetch the data from the random user API
try {
results = await fetchUserData(endpointURL);
// console.log({results});
sessionStorage.setItem('userdata', JSON.stringify(results));
// console.log('fetch call made');
// console.log('Session storage used');
// console.log('--------------------');
} catch (error) {
console.log('Error:', error);
}
}

Caching Sorted Tables in the Event Listener (Memoization)

I had to demonstrate memoization for a few technical interviews recently. I wanted to implement memoization so that the table did not need to run a sort function every time the column button is clicked.

So I initially create a cache within the event listener function and return a function that will be used when the column button is clicked.

function saveToCache() {
let cache = {};
/**
* Sorts the table in ascending/descending order and updates the view of the table
*
* @param {HTMLTableElement} table The desired table that needs to be sorted
* @param {Number} column The index of the column to sort
* @param {Boolean} asc Determines if the sorting will be in ascending/descending order
* @return {Boolean} Returns true when the function completes properly
*/
return (table, column, asc = true) => {

Win the return function, we will try to access the cache to see if cache[${order}${column}] (example cache['ascending1'] for column 1 in ascending order). if it does not exist, we will perform the sort.

if (cache[`${order}${column}`]) {
// console.log('cache has been used');
// Since it is available, we will use the sorted array stored in cache
sortedRows = cache[`${order}${column}`];
} else {
// Sort each row
sortedRows = rows.sort((a, b) => {
const aColumn = a.querySelector(`.c-table__td:nth-child(${column + 1})`);
const bColumn = b.querySelector(`.c-table__td:nth-child(${column + 1})`);
let aColumnContent;
let bColumnContent;
switch (column) {
case 3:
// If it is 'IMAGES' column (4th), use the data-id attribute within the <img> element
aColumnContent = aColumn.getAttribute('data-id');
bColumnContent = bColumn.getAttribute('data-id');
break;
case 5:
// In the 'Address' column (6th), only use the street number from the address to sort
aColumnContent = aColumn.textContent.split(' ')[0];
bColumnContent = bColumn.textContent.split(' ')[0];
// console.log({aColumnContent, bColumnContent})
// console.log('sorted by street number');
break;
case 9:
// If both values can be converted into a Date value, convert it
// Just splitting the date (MM/DD/YYYY) by / and using the year (YYYY) for sorting
aColumnContent = new Date(aColumn.textContent.trim());
bColumnContent = new Date(bColumn.textContent.trim());
// console.log({ aColumnContent, bColumnContent });
// console.log('sorted by date');
break;
default:
// Default will be HTML Content as a String
aColumnContent = aColumn.textContent.trim();
bColumnContent = bColumn.textContent.trim();
}
// console.log({ aColumnContent, bColumnContent })
// If both values can be converted into a Number value, convert it to a number
if (!Number.isNaN(parseInt(aColumnContent)) && !Number.isNaN(parseInt(bColumnContent))) {
aColumnContent = parseInt(aColumnContent);
bColumnContent = parseInt(bColumnContent);
// console.log('sorted by number');
}
return aColumnContent > bColumnContent
? 1 * directionModifier
: bColumnContent > aColumnContent
? -1 * directionModifier
: 0;
});
// Store the asc/desc sorted rows in the cache for future reference
cache[`${order}${column}`] = sortedRows;
// console.log({cache})
// console.log({sortedRows});
}

After the sort is performed, we will store it in the cache object for future reference.

cache[`${order}${column}`] = sortedRows;

we will call saveToCache and name the returning function sortByTableColumn

let sortTableByColumn = saveToCache();

We then call sortByTableColumn in the click listener. Notice I create an event listener on the document and add a conditional for make sure the button with a class js-column-button is the only element that the sorting function will work.

function handleClick(event) {
// the function will only run when a <th> is clicked on
if (event.target?.closest('.js-column-button')) {
// Get Column ID number
const columnIndex = parseInt(event.target.getAttribute('data-col'));
// Check if span.c-table__button--icon has the ascending icon class and return boolean (true/false)
const currentIsAscending = event.target.firstElementChild?.classList?.contains('c-table__button--asc');
sortTableByColumn(table, columnIndex, !currentIsAscending);
}
return false;
}

// Click Event Listener
document.addEventListener('click', handleClick);

Back to Top

Using Modern CSS

CSS Custom Properties

This Kevin Powell YouTube video on CSS custom properties really helped me better understand how to use these properties. Here is an example. I created 5 custom properties in my body selector so I can use the custom properties within all nested elements

body {
/*Custom Properties*/
--header-background-color: #0074d9;
--main-background-color: #ffffff;
--main-background-accent-color: #dddddd;
--main-text-color: #111111;
--error-text-color: #ff4136;
color: var(--main-text-color);
font-family: sans-serif;
line-height: 1.25;
}

Now I have a custom property called --main-text-color that stores the hex code of black (#111111). But since my button is going to be blue, I would like my text color to be white (#ffffff). Rather than create another custom property, I can overwrite (or locally scope) the property within a selector and use the same property name like so:

.c-table__button {
--main-text-color: #ffffff;
align-items: stretch;
background-color: var(--header-background-color);
border: 1px transparent solid;
color: var(--main-text-color);

Now the text color within my button will be white (#ffffff)

BEM naming convention

After reading the namespace section of this Smashing Magazine article on mistakes to avoid using BEM (Block, Element, Modifier), I decided to apply the same BEM prefix namespacing as in the Smashing Magazine article. Below is the table that shows the prefix description and examples from the article.

Type Prefix Examples Description
Component c- c-card
c-checklist
Form the backbone of an application and contain all of the cosmetics for a standalone component.
Layout module l- l-grid
l-container
These modules have no cosmetics and are purely used to position c- components and structure an application’s layout.
Helpers h- h-show
h-hide
These utility classes have a single function, often using !important to boost their specificity. (Commonly used for positioning or visibility.)
States is-
has-
is-visible
has-loaded
Indicate different states that a c- component can have.
JavaScript hooks js- js-tab-switcher These indicate that JavaScript behavior is attached to a component. No styles should be associated with them; they are purely used to enable easier manipulation with script.
— David Berner

Source of namespacing: Harry Robert - More Transparent UI Code with Namespaces

As a result I used the following block class names:

  • Components - c-header, c-table
  • Elements - c-header__title, c-table__td
  • Layout - .l-table-container, .l-loading-container
  • States - is-loading, has-error

Back to Top

Additional Features

  1. Pagination for multiple pages of results
  2. In tablet/mobile view, add tab functionality to focus on each card full of data
  3. Add an input to search on the table and highlight

Back to Top

Resources

Accessibility Links

HTML Links

CSS Links

JavaScript Links

Back to Top

Author

Back to Top

Acknowledgments

  • @sw-yx @techieEliot @rayning0 and @amhayslip pushing the dev community (including myself) to learn and grow in public.
  • @kevin-powell for making me smarter about CSS
  • @cferdinandi for making me smarter about JavaScript.

Back to Top

About

A simple responsive single page that takes data from the Random User Generator API and then builds a fully accessible HTML table. The column can sort when the user clicks the column header button with a mouse or uses the enter/space key.

Topics

Resources

License

Stars

Watchers

Forks