This lab would take you to a solution to a sample lab exam. The lab exam intends to assess students' ability to build a modern web application using HTML/CSS/JS without using any 3rd party libraries. This lab will also look at some advanced features you may opt to add to your application.

Exam Specification

The format of the lab is open book, open ended. You will be provided with a selection of REST APIs and you can build any application of your choosing.
Minimalistically your application should contain the following:

  1. Appropriate Markup
  2. Reasonable Styling
  3. Proper Layout (using flex/grid)
  4. AJAX, ie rendering data on the page from an API
  5. Interactivity using forms or buttons

The API given for the sample exam is https://amiiboapi.com/.

There are many types of requests you can make to this API but this solution will use the following https://amiiboapi.com/api/amiibo/ which returns the following data.

Time is of the essence in the exam so you should aim to build the most minimal application that meets the requirements. In this solution, the app shall comprise 3 main components, a navbar, a control's section and a table in a column.

The table shall show the data from the API and the controls section will contain components that will change the data on the screen. The table shall show the character, gameSeries, image and the na release date. Some sample markup can be made based on the api data we would like to show.

<body>
  <nav>
    Amiibo App
  </nav>
  <main>
  
    <section>
    </section>

    <table>
     <thead>
        <tr>
         <th>Name</th>
         <th>Series</th>
         <th>Image</th>
         <th>Release</th>
       </tr>
     </thead>
     <tbody>
       <tr>
         <td>Mario</td>
         <td>Super Mario</td>
         <td>
           <img src="https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_00000000-00000002.png">
         </td>
         <td>
           2014-11-21
         </td>
       </tr>
     </tbody>
    
    </table>
 
   </main>
</body>

The page should now look like the following

Next we shall style the body to be a flex column, add a background, and remove padding and margins. We also set the box-sizing for all elements so adding padding would not affect the size of our elements.

    *{
        box-sizing: border-box;
     }

     body{
        display: flex;
        padding: 0;
        margin: 0;
        background-color: gainsboro;
        flex-direction: column;
        gap: 10px;
     }

We then style our navbar, main and section components on the page.

    nav {
      padding: 10px;
      color: white;
      font-size: 25px;
      font-weight: bold;
      font-family: monospace;
      background-color: blue;
      display: flex;
      flex-grow: 1;
      box-shadow: 0px 10px 5px 0px rgba(0,0,0,0.31);
    }

    main{
      margin-right: auto;
      margin-left: auto;
      width: 95vw;
      display: flex;
      flex-grow: 10;
      flex-direction: column;
      gap: 5px;
    }

    section{
      display: flex;
      align-items: center;
      justify-content: space-evenly;      
    }

Finally we add styles to our table then a card class to apply its styles to the section and table.

    .card{
      border-radius: 2px;
      background-color: white;
      font-size: 0.7em;
      padding: 8px;
      box-shadow: -11px 9px 5px -4px rgba(0,0,0,0.31);
      border: 1px solid gainsboro;
    }

    table{
      border-collapse: collapse; //removes the default table borders
    }

    thead, th{
      background-color: blue;
      color: white;
      font-weight: bold;
      font-size: 1.5em;
    }

    td {
      font-size: 1.5em;
      text-align: center;
    }

    tr:nth-child(even){
      background-color: #f2f2f2;
    }

    tr:hover {
      background-color: #ddd;
    }

    table img{
      height: 100px;
    }

Finally we add the card class to the relevant child elements of main.

  <section class="card">

  </section>
   
  <table class="card">
     <thead>
        <tr>
         <th>Name</th>
         <th>Series</th>
         <th>Image</th>
         <th>Release</th>
       </tr>
     </thead>
     <tbody>
        <tr>
         <td>Mario</td>
         <td>Super Mario</td>
         <td>
           <img src="https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_00000000-00000002.png">
         </td>
         <td>
           2014-11-21
         </td>
       </tr>
     </tbody>
   </table>

This would give the following result

Next we shall add some JavaScript code that would make a call to the API and render its data in the table (AJAX).

First we need to add an id to an element that will be our render target.

<tbody id="result">
</tbody>

Then we create a request function getData() to request the data from the API then pass it to a render function drawTable().

getData() is called when the page is loaded.

function drawTable(records){
  
  let result = document.querySelector('#result'); //get render target
  
  //build html string based on data
  let html='';

  for(let rec of records){
   //interpolate data into template
    html+=`<tr>
     <td>${rec.character}</td>
     <td>${rec.gameSeries}</td>
     <td>
       <img src="${rec.image}">
     </td>
     <td>
       ${rec.release.na}
     </td>
   </tr>`;
  }
  //add html string to the DOM
  result.innerHTML = html;
}

async function getData(){
  const response = await fetch('https://amiiboapi.com/api/amiibo/');
  const data = await response.json();
  drawTable(data.amiibo.slice(0, 100)); 
  //slice method is used to use the first 100 records
}

getData();

This should produce the following result.

At this point this solution should at least make 90%. Adding some interactivity would be required for the final rubric. You can view the implementation of this solution at the link below.

Solution Repl

State management in web apps refers to how an application manages its data (variables & data structures) that it stores in memory. Advanced interactivity in applications typically requires manipulating or projecting application state.

The link below shows an alternative solution that caches the API data in a global array which lets us perform state management.

Solution 2 Repl

In this implementation drawTable() is removed from getData(). Instead showAllGames() is called when the page is loaded and passes the data from getData() to drawTable().

let state = [];//global state stores the API data

async function getData(){
  const response = await fetch('https://amiiboapi.com/api/amiibo/');
  const data = await response.json();
  return data.amiibo.slice(0, 100);
  //slice method gets first 100 items (too much data from API)
}

//show all games loads our global state with the data then
//calls drawTable
async function showAllGames(){
    state = await getData();
    drawTable(state);
}

showAllGames();

We can implement filtering controls to our application which will filter our dataset by a particular value of a field. First we add the following markup. The onclick handlers of the buttons call a filter function with a different value. These values are the possible values of the gameSeries field of the data in the dataset.

  <section class="card">
    <button onclick="filterBySeries('Animal Crossing')">
      Animal Crossing
    </button>

    <button onclick="filterBySeries('Breath of the Wild')">
      Breath of the Wild
    </button>

    <button onclick="filterBySeries('Super Mario')">
      Super Mario
    </button>
  </section>

The filterBySeries() function creates a filtered array from the global state then passes it to drawTable().

function filterBySeries(series){

  let filtered = [];
  
  //iterate the records in the global state
  for(let rec of state){
    
    //push only elements that match the given series
    if (rec.gameSeries === series)
      filtered.push(rec);
  }
  drawTable(filtered);
}

This will add filtering functionality to the table.

Next we can add a search feature that will make an array with the relevant search results from global state then pass it to drawTable().

First we add markup for a search field and button.

  <section class="card">
    <button onclick="filterBySeries('Animal Crossing')">
      Animal Crossing
    </button>

    <button onclick="filterBySeries('Breath of the Wild')">
      Breath of the Wild
    </button>

    <button onclick="filterBySeries('Super Mario')">
      Super Mario
    </button>

    <input type="search" id="searchKey" placeholder="search name">
    <button onclick=search()>Search</button>

  </section>

Then we can implement a search function that pulls out the relevant records from the global state.

function search(){
  //get the search term from the input field
  let searchKey = document.querySelector('#searchKey').value;
  
  let results = [];

  //iterate all the records in the global state
  for(let rec of state){

    //capitalize the search term and text to be case insensitive
    let searchText = rec.character.toUpperCase();
    searchKey = searchKey.toUpperCase();

    //add to resulting array if search term is in the text
    if ( searchText.search(searchKey) !== -1 ){
      results.push(rec);
    }

    //draw table with the search results
    drawTable(results);
  }
}

This will result in a working search bar for the table.

Finally we can implement a sort function that will create a new array with the elements from state rearranged then pass it to drawTable().

First we add a sort button to the Name table heading.

  <th>Name <button onclick="sortByName()">Sort</button> </th>

Then a style for it.

thead button {
  height: 20px;
  font-size: 10px;
  font-weight: bold;
}

We can use the JavaScript Array.sort() and.String.localCompare() function to compare the character fields of the records.

//compares the character fields of the items in the array
function nameCompare(a, b){
  return a.character.localeCompare(b.character);
}

function sortByName(){
  let result = state.sort(nameCompare);
  drawTable(result);
}

This will result in a working sort feature for the table.

The implementation can be found at the link below.

Repl Workspace

When referring to the API documentation, it shows how you can provide an optional argument "showusage" to the url to pull the game titles associated with the amiibo in the gamesSwitch property of the data.

We can implement a user experience that will let the user select a record and show the related games on the page. First, we change the layout to make space to show the games.

<article>
  <div class="scrollable">
     <table class="card">
       <thead>
          <tr>
           <th>Name <button onclick="sortByName()">Sort</button> </th>
           <th>Series</th>
           <th>Image</th>
           <th>Release</th>
           <th>Action</th>
         </tr>
       </thead>
       <tbody id="result">
       </tbody>
     </table>
  </div>
  
  <aside>
    <h1>Supported Games For <span id="character"></span></h1>
    <ul id="games"></ul>
  </aside>
</article>

We now have two more render targets character and games which will show the selected character name and its relevant games respectively.

Then we add some styles to adjust the layout and define a scrollable class so the user can scroll the table.

article{
  display: flex;
  flex-direction: row;
};

aside{
  display: flex;
  flex-grow: 2;
  background-color: white;
  flex-direction: column;
}

.scrollable{
  display:flex;
  flex-grow: 4;
  max-height: 80vh;
  overflow-y:scroll;
}

table{
  border-collapse: collapse;
  width: 100%;
}

Finally we update our row template in drawTable() to add a column with a details button.

function drawTable(records){
  let html='';
  let result = document.querySelector('#result');
  
  for(let rec of records){
    html+=`<tr>
     <td>${rec.character}</td>
     <td>${rec.gameSeries}</td>
     <td>
       <img src="${rec.image}">
     </td>
     <td>
       ${rec.release.na}
     </td>
     <td>
       <button onclick="viewDetails('${rec.head}', '${rec.tail}')">View Details</button>
     </td>
   </tr>`;
   
  }

  result.innerHTML = html;

}

This should produce the following result.

Then we can implement show details as below.

//retrieves a record from state using the head and tail
function getRecord(head, tail){
  for(let rec of state){
    if (rec.head === head && rec.tail === tail){
      return rec;
    }
  }
}

//shows the character name and related games on the screen
function viewDetails(head, tail){

  let record = getRecord(head, tail);

  let character = document.querySelector('#character');
  let result = document.querySelector('#games');

  let html = '';

  for(let game of record.gamesSwitch){
    html+=`<li>${game.gameName}</li>`;
  }
  
  character.innerHTML = record.character;
  result.innerHTML = html;
}

We create a getRecord function that retrieves the relevant record from the global state. This will result in the following. The "View Details" button will show the relevant games to the right.

And that's the end of the lab. Hopefully it can serve as a guide for your future implementations.

The completed version of the lab can be found below.

Completed Repl

References & Additional Reading

JS Sorting arrays

JS Filter