- Notifications
You must be signed in to change notification settings - Fork1
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
abuna1985/sortable-table
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- Table Of Contents
- About The Project
- Requirements
- My Process
- Built with
- What I Learned
- Additional Features
- Resources
- Author
- Acknowledgments
This project is a simple single-page responsive design which takes data from theRandom 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 aclick
of a mouse and/orenter/space
key.
Read theAPI 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 datatitle
: (string) title (Mr., Ms, etc)first
: (string) first namelast
: (string) last name
location
: (object) contains location datastreet
: (string) street number and namecity
: (string)citystate
: (string) statepostcode
: (string) zip/postal code
coordinates
: (object) contains coordinates datalatitude
: (string) latitudelongitude
: (string) longitude
timezone
: (object) contains time zone dataoffset
: (string) timezone offsetdescription
: (string) time zone
email
: (string) email addresslogin
: (object) contains login datauuid
: (string) unique user id,username
: (string) usernamepassword
: (string) passwordsalt
: (string) salt hashmd5
: (string) md5 hashsha1
: (string) sha1 hashsha256
: (string) sha256 hash
dob
: (object) contains age datadate
: (timestamp) date of birthage
: (number) age of person
registered
: (object) contains registration datadate
: (timestamp) registrationage
: (number) age of membership
phone
: (string) phone numbercell
: (string) cell phone numberid
: (object) contains id dataname
: (string) id namevalue
: (string) id value
picture
: (object) contains picture datalarge
: (string) URL of large imagemedium
: (string) URL of medium imagethumbnail
: (string) URL of thumbnail image
nat
: (string) nationality
- Use the result a from theRandom User Generator API
- Use HTML, CSS and Javascript to show the data in a readable table (including mobile view)
- 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 inmobile
andtablet
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
- Direction Keys
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)
Since the requirements were fetching API data and rendering a table, I approached it the following way:
Create anindex.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.ec-header
andc-table
).
Create ascript.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
- render the contents of the
- 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 theprevious HTML element with a
tabindex
attribute
- Move the focus to theprevious HTML element with a
right arrow
,down arrow
,d
,s
- Move the focus to thenext HTML element with a
tabindex
attribute
- Move the focus to thenext HTML element with a
home
- Move the focus to thefirst HTML element with a
tabindex
attribute
- Move the focus to thefirst HTML element with a
end
- Move the focus to thelast HTML element with a
tabindex
attribute
- Move the focus to thelast HTML element with a
- Click
- AddNormalize.css to reset the CSS browser defaults
- Create a
style.css
and add styles to:
- Header -
.c-header
- Header Title -
.c-header__title
- Header Subtitle -
.c-header__subtitle
- Header Title -
- Table -
c-table
- table header -
c-table__head
- header cell (th) -
.c-table__th
- button -
.c-table__button
- button -
- header cell (th) -
- table body -
c-table__body
- table row (tr)
.c-table__tr
- table data (td)
.c-table__td
- table data (td)
- table row (tr)
- table header -
- Loading Screen -
.l-loading-container
.is-loading
- Animations
move
keyframe animationgrow
keyframe animation
- Mobile view styling
@media screen and (max-width: 768px)
- Semantic HTML5
- CSS3
- Normalize.css
- CSS Animation (loading screen)
- CSS custom properties
- BEM naming convention
- ES6 JavaScript
- Async/Await
- Fetch
- Closures/Memoization
After reviewing theDeque 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:
<tablerole="grid"aria-readonly="true"><thead><trrole="row"><throle="columnheader"scope="col">col 1<th><throle="columnheader"scope="col">col 2<th><throle="columnheader"scope="col">col 3<th></tr></thead><tbody><trrole="row"><thscope="row"role="rowheader">data 1</th><tdrole="gridcell">data 2</td><tdrole="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.
In most API fetching demos, the API call is made as the page is rendered. I decided to useSessionStorage
to store the initial API call data. After the initial fetch, the table will pull the data directly fromSessionStorage
.
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:
sortable-table/assets/js/script.js
Lines 342 to 360 in891f3f9
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=awaitfetchUserData(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); | |
} | |
} |
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.
sortable-table/assets/js/script.js
Lines 71 to 81 in891f3f9
functionsaveToCache(){ | |
letcache={}; | |
/** | |
* 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 ifcache[
${order}${column}]
(examplecache['ascending1']
for column 1 in ascending order). if it does not exist, we will perform the sort.
sortable-table/assets/js/script.js
Lines 95 to 149 in891f3f9
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)=>{ | |
constaColumn=a.querySelector(`.c-table__td:nth-child(${column+1})`); | |
constbColumn=b.querySelector(`.c-table__td:nth-child(${column+1})`); | |
letaColumnContent; | |
letbColumnContent; | |
switch(column){ | |
case3: | |
// 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; | |
case5: | |
// 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; | |
case9: | |
// 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=newDate(aColumn.textContent.trim()); | |
bColumnContent=newDate(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'); | |
} | |
returnaColumnContent>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 thecache
object for future reference.
sortable-table/assets/js/script.js
Line 146 in891f3f9
cache[`${order}${column}`]=sortedRows; |
we will callsaveToCache
and name the returning functionsortByTableColumn
sortable-table/assets/js/script.js
Line 16 inf95667b
letsortTableByColumn=saveToCache(); |
We then callsortByTableColumn
in the click listener. Notice I create an event listener on the document and add a conditional for make sure the button with a classjs-column-button
is the only element that the sorting function will work.
sortable-table/assets/js/script.js
Lines 327 to 337 in891f3f9
functionhandleClick(event){ | |
// the function will only run when a <th> is clicked on | |
if(event.target?.closest('.js-column-button')){ | |
// Get Column ID number | |
constcolumnIndex=parseInt(event.target.getAttribute('data-col')); | |
// Check if span.c-table__button--icon has the ascending icon class and return boolean (true/false) | |
constcurrentIsAscending=event.target.firstElementChild?.classList?.contains('c-table__button--asc'); | |
sortTableByColumn(table,columnIndex,!currentIsAscending); | |
} | |
returnfalse; | |
} |
sortable-table/assets/js/script.js
Lines 366 to 367 in891f3f9
// Click Event Listener | |
document.addEventListener('click',handleClick); |
ThisKevin 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 mybody
selector so I can use the custom properties within all nested elements
sortable-table/assets/css/style.css
Lines 2 to 13 in3f0fa71
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:
sortable-table/assets/css/style.css
Lines 81 to 87 in3f0fa71
.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
)
After reading thenamespace 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.
— David Berner
Type Prefix Examples Description Component c- c-card
c-checklistForm the backbone of an application and contain all of the cosmetics for a standalone component. Layout module l- l-grid
l-containerThese modules have no cosmetics and are purely used to positionc- components and structure an application’s layout. Helpers h- h-show
h-hideThese 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-loadedIndicate 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.
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
- Pagination for multiple pages of results
- In tablet/mobile view, add tab functionality to focus on each card full of data
- Add an input to search on the table and highlight
- Web AIM - Creating accessible tables
- MDN - HTML table advanced features and accessibility
- Deque University - Sortable table example
- W3 - Sortable table example
- Codepen - David Miller - Responsive table example
- MDN Docs - table-layout
- LogRocket- CSS animated page loading
- Smashing Magazine - David Berner - Battling BEM CSS: 10 Common Problems And How To Avoid Them
- CSS Wizardry - Harry Robert - More Transparent UI Code with Namespaces
- Go Make Things - JavaScript Event Delegation - This helped me better understand how event delegation works in JavaScript.
- Go Make Things - Inject text and HTML with JavaScript - This article helped me as a reference.
- Random User API Documentation Here is the documentation for the Random User API for reference.
- JavaScript.info - Optional Chaining
- MDN - switch statement
- Go Make Things - Chris Ferndinandi - JavaScript format date helper function
- Mastering JS - Date object
- freeCodeCamp - Understanding JavaScript Memoization
- Website -adamabundis.xyz
- GitHub -@abuna1985
- Twitter -@adamabundis
- @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.
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.