Tehnologii Web/2022-2023/Laborator 6

In this laboratory, we will review the presented front-end techniques in a data table page.

  • You will start from a partial implementation - or you can start your own.
  • Steps 1-5 are provided to you, the rest are lab assignments.

Requirements edit

  • Fetch an existing API endpoint (e.g. GET https://dummyjson.com/users) using Typescript and fetch
  • Add the table rows dynamically.
  • Add pagination support.
  • Sort the entries by column: ascending or descending.
  • Filter rows by search.
  • Bonus: let the user download the data using .CSV export.
 
Lab assignment - data table

Laboratory Steps edit

Step 1 edit

Add the HTML markup for your page. It should look like the one below.

The table headers will be statically defined, the rest of the content will be loaded dynamically.

<div id="app">
  <table id="table">
      <thead>
        <tr>
          <td>First Name</td>
          <td>Last Name</td>
          <td>Phone Number</td>
          <td>Email</td>
          <td>Picture</td>
        </tr>
      </thead>
      
      <tbody id="table-body">
       <!-- Table rows will load here. -->
      </tbody>
  </table>
  
  <div id="table-pagination">
    <!-- Pagination buttons will load here. -->
  </div>
</div>

Step 2 edit

Add an interface for your data. In our example (GET https://dummyjson.com/users ) we will expose the following fields.

interface Person {
  firstName: string;
  lastName:  string;
  email: string;
  phone: string;
  image: string;
}

Step 3 edit

Add a Typescript class controller. We will define a load method in the next step.

class DataTableController { 
    sourceURL: string;
    itemsPerPage: number;
  

	constructor(sourceURL: string, itemsPerPage: number) {
  	    this.sourceURL = sourceURL;
        this.itemsPerPage = itemsPerPage;
    }
}

const controller = new DataTableController('https://dummyjson.com/users', 10);
controller.load();

Step 4 edit

Our load method will use the fetch call to load the JSON data from an external source.

Please note, we will use a private field _data to store our data, but our table view will be defined in the getter function tableData which is public.

The spread ... operator will make a copy since sort is a mutable call.

load = () => {
    fetch(this.sourceURL)
    	.then(data => data.json())
        .then(json => {
            this._data = json.users;
            // display the first page on load
            this.goToPage(0);
        }
    );
}
  
get tableData(): Person[] {
    let data = [...this._data]
	    .filter(x => true); 
    // TODO: add your own filter
    // TODO: add your own sorting function
    data.sort();
    return data;
}

Step 5 edit

Make sure your class and helper methods are defined as follows.

interface Person {
  firstName: string;
  lastName:  string;
  email: string;
  phone: string;
  image: string;
}


class DataTableController { 
    // TODO: Use these private fields.
    private _search: string = '';
    private _sortField: 'firstName';
    private _ascending: true;
    private _currentPageIdx: number = 0;
  
    public sourceURL: string;
    private _data: Person[];


	constructor(sourceURL: string, itemsPerPage: number) {
  	    this.sourceURL = sourceURL;
        this.itemsPerPage = itemsPerPage;
    }
  
    load = () => {
      fetch(this.sourceURL)
        .then(data => data.json())
        .then(json => {
            this._data = json.users;
            this.goToPage(0);
          }
        );
    }
  
  get tableData(): Person[] {
  	let data = [...this._data]
    	.filter(x => true); // TODO: add your own filter
    // TODO: add your own sorting function
    data.sort();
    return data;
  }
  
  
  goToPage = (pageIndex: number) => {
  	this.clear();
  	for (let i = 0; i < this.itemsPerPage; i++) {
    	this.addRow(this.tableData[pageIndex * this.itemsPerPage + i]);
    }
    this.addPaginationButtons();
  }
  
  clear = () => {
  	document.getElementById('table-pagination')!.innerHTML = '';
  	document.getElementById('table-body')!.innerHTML = '';
  }
  
  numberOfPages = (length: number) => (
  	length / this.itemsPerPage + 
    (length % this.itemsPerPage > 0 ? 1 : 0);
  )
  
  addPaginationButtons = () => {
    for (let i = 1; i <= this.numberOfPages(this.tableData.length); i++) {
        const element = document.createElement('span');
        element.innerHTML = `${i}`;
     	element.onclick = () => {
      	    this.goToPage(i - 1);
        }
        document.getElementById('table-pagination')!.appendChild(element);
    }
  }
  
  addRow = (person: Person) => {
  	const element = document.createElement("tr");
    element.innerHTML = `
      <td>${person.firstName}</td>
      <td>${person.lastName}</td>
      <td>${person.phone}</td>
      <td>${person.email}</td>
      <td>${person.image}</td>`;
  	document.getElementById('table-body')!.appendChild(element);
  }
}

const controller = new DataTableController('https://dummyjson.com/users', 10);
controller.load();

Step 6 edit

Your Task

  • Set the class name for the active pagination button accordingly.
  • You can do that in the method addPaginationButtons by setting element.className = isActivePage ? 'A' : 'B'
  • You also need to set the private this._currentPageIdx in your goToPage method
  • Add a CSS transition, you can use also the one below which is animating the width.
.pagination-btn:after {
  width: 0px;
  transition: width ease-in-out 0.5s;
  content: "";
}

.pagination-btn.active:after {
  content: "";
  width: 80px;
  height: 4px;
  background: #5FA4F9;
  position: absolute;
  left: 0;
  right: 0;
  bottom: -40px;
  margin: 0 auto;
}

Step 7 edit

Your Task

  • Set in the constructor a query selector for each column header.
  • Add a click event listener, whenever you click a column, the private field _sortField would be set accordingly.
  • When you click the second time, the _ascending field would toggle from true to false or from false to true.

Step 8 edit

Your Task

  • Update the tableData getter to implement search filtering and sorting.

Step 9 edit

Bonus Task - add an Export button that will prompt the user to download the data in .csv format.

You can do it from scratch using blobs or using external Javascript libraries.