Vanilla JavaScript app with REST API

Vanilla JavaScript app with REST API

In this article we're going to see how to implement a simple app to consume a REST API with Vanilla JavaScript.

In this article we're going to see how to implement a simple app to consume a REST API with Vanilla JavaScript.

Our API returns the temperatures taken on specific days. Each record has a date and the minimum and maximum values for each day in the list. The API returns paginated data so that we have the total number of pages and the current page number.

What we want to build is an app that shows the list of records plus a series of features, which include:

  1. Pagination
  2. Conversion between Fahrenheit and Celsius degrees
  3. Sorting by date, minimum temperature and maximum temperature.

Let's start by defining our base HTML structure:

<main class="app">
        <div class="container">
            <div class="temperatures-actions">
                <form id="sort-form">
                    <label for="sort-key">Sort by:</label>
                    <select id="sort-key">
                        <option value="date_taken">Date</option>
                        <option value="min">Minimum</option>
                        <option value="max">Maximum</option>
                    </select>
                    <label for="sort-order">Order:</label>
                    <select id="sort-order">
                        <option value="asc">Ascending</option>
                        <option value="desc">Descending</option>
                    </select>
                    <button type="button" id="sort-btn">Sort</button>
                </form>
                <div class="unit-actions">
                    <span>Unit:</span>
                    <button data-unit="C">Celsius</button>
                    <button data-unit="F" class="active">Fahrenheit</button>
                </div>
            </div>
            <header class="temperatures-header">
                <div>Date</div>
                <div>Minimum</div>
                <div>Maximum</div>
            </header>
            <ul class="temperatures"></ul>
            <nav class="pagination">
                <button type="button" id="previous-btn" disabled>Previous</button>
                <button type="button" id="next-btn">Next</button>
            </nav>
        </div>
    </main>

The first thing to do in our JavaScript code is to define the base URL of the API and a state object that will help us to keep track of the data returned by the API each time we perform an AJAX request.

const API_URL = 'https://weather.tld/api/';
const AppData = {
    temperatures: [],
    page: 1,
    pages: 1,
};

Now we can create a function that uses the JavaScript Fetch API to perform a GET request to the remote endpoint.

async function getTemperatures(page = 1) {
    try {
        const response = await fetch(`${API_URL}temperatures?page=${page}`);
        
        if(!response.ok) {
            throw new Error('Request error');
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        return null;
    }
}

The response object contains the ok property that can be read in order to know whether the request returns an HTTP OK status (200) or not. If there's an error, the control is taken by the catch block, otherwise we return the JSON object of the response. Our function accepts a page number as its argument in order to be reused later for implementing the pagination logic.

Once we get the results, we need to create the DOM structure of our main list by iterating through the results.

function displayResults(results) {
        const list = document.querySelector('.temperatures');
        list.innerHTML = '';
        for (const result of results) {
            const item = document.createElement('li');
            item.innerHTML = `
                <span class="temperature-date">${new Date(result.date_taken).toLocaleDateString()}</span>
                <span class="temperature-min">${result.min.toFixed()}</span>
                <span class="temperature-max">${result.max.toFixed()}</span>
            `;
            list.appendChild(item);
        }
    }

Later on we'll implement the conversion between Fahrenheit and Celsius degrees and since this operation will produce numbers that carry a decimal part, we need to truncate that part by calling the toFixed() method without arguments.

We also need to define an error handler function:

function handleError(message = 'Error') {
    const list = document.querySelector('.temperatures');
    const err = document.createElement('div');
    err.className = 'app-error';
    err.textContent = message;
    list.before(err);    
}

Now it's time to combine the data retrieval logic with its representation in the DOM by creating a dedicated function.

function fetchData(page = 1, fn = () => {}) {
   getTemperatures(page).then(data => {
            if (data) {
                displayResults(data.results);
                AppData.temperatures = data.results;
                AppData.page = data.current_page;
                AppData.pages = data.pages;
                sessionStorage.setItem('data', JSON.stringify(AppData));
                fn(AppData);
            }
    }).catch(err => {
        handleError('Request error.');
    })
}

If the request was successful, we display the results for the given page and we store the relevant properties in our state object. Also, since we're going to deal with the scope created by callback functions, we save our state in the web storage for later retrieval. Finally, we invoke the optional callback function by using the state object as its parameter.

The first feature to implement now is data pagination. We need to toggle the disabled state in the pagination buttons depending on the comparison between the current page and the total number of pages available.

function togglePaginationButtons() {
        const data = JSON.parse(sessionStorage.getItem('data'));
        const nextBtn = document.querySelector('#next-btn');
        const previousBtn = document.querySelector('#previous-btn');
        if (data.page === 1) {
            previousBtn.setAttribute('disabled', 'disabled');
        } else {
            previousBtn.removeAttribute('disabled');
        }
        if (data.page === data.pages) {
            nextBtn.setAttribute('disabled', 'disabled');
        } else {
            nextBtn.removeAttribute('disabled');
        }
    }

This function will be called once we get the results retrieved by requesting a specific page:

function nextPage() {
        const data = JSON.parse(sessionStorage.getItem('data'));
        if (data.page < data.pages) {
            fetchData(data.page + 1, function (appData) {
                togglePaginationButtons();
            });
        }
    }

    function previousPage() {
        const data = JSON.parse(sessionStorage.getItem('data'));
        if (data.page > 1) {
            fetchData(data.page - 1, function (appData) {
                togglePaginationButtons();
            });
        }
    }
    
    function handlePaginationButtons() {
        const nextBtn = document.querySelector('#next-btn');
        const previousBtn = document.querySelector('#previous-btn');
        nextBtn.addEventListener('click', nextPage, false);
        previousBtn.addEventListener('click', previousPage, false);
    }

For the conversion of temperatures we need to create an helper function that implements the two mathematical formulas required for this task:

function convertTemperature(temperature, unit = 'C') {
        if (unit === 'C') {
            return (temperature - 32) * 5 / 9;
        }
        return (temperature * 9 / 5) + 32;
}

In this function, C stands for Celsius and F for Fahrenheit. Now in order to convert the data already inserted in our list, we need to read from the web storage, convert the JSON string into a JSON object and retrieve the original data which are returned in Fahrenheit notation.

function convertTemperatures(unit = 'C') {
        const data = JSON.parse(sessionStorage.getItem('data'));
        fetchData(data.page, appData => {
            const temperatures = appData.temperatures;
            const convertedTemperatures = [];
            for (const temperature of temperatures) {
                const convertedTemperature = {
                    date_taken: temperature.date_taken,
                    min: convertTemperature(temperature.min, unit),
                    max: convertTemperature(temperature.max, unit),
                };
                convertedTemperatures.push(convertedTemperature);
            }
            let results = temperatures
            if (unit === 'C') {
                results = convertedTemperatures;
            }
            displayResults(results);
        });
    }

The next step consists of associating the above function to the click event handler of the two HTML buttons.

function handleConversionButtons() {
        const buttons = document.querySelectorAll('button[data-unit]');
        for (const button of buttons) {
            button.addEventListener('click', event => {
                const unit = event.target.dataset.unit;
                convertTemperatures(unit);
                const activeBtn = document.querySelector('.temperatures-actions button.active');
                if (activeBtn) {
                    activeBtn.classList.remove('active');
                }
                event.target.classList.add('active');
            }, false);
        }
    }

The last feature that needs to be implemented is sorting. Since each record has the date_taken, min and max properties, one of our select elements shows three option elements with exactly the same values so that we can use these values to access the corresponding properties on each object contained in the JSON array.

function sortTemperatures(key = 'min', order = 'asc') {
        const data = JSON.parse(sessionStorage.getItem('data'));
        fetchData(data.page, appData => {
            const temperatures = appData.temperatures;
            const sortedTemperatures = temperatures.sort((a, b) => {
                let keyA = key === 'date_taken' ? new Date(a[key]) : a[key];
                let keyB = key === 'date_taken' ? new Date(b[key]) : b[key];
                if (order === 'asc') {
                    return keyA - keyB;
                }
                return keyB - keyA;
            });
            displayResults(sortedTemperatures);
        });
    }

The function defined above can be invoked when a user clicks on the "Sort" button:

function handleSortButton() {
        const button = document.querySelector('#sort-btn');
        button.addEventListener('click', () => {
            const key = document.querySelector('#sort-key').value;
            const order = document.querySelector('#sort-order').value;
            sortTemperatures(key, order);
        }, false);
    }

Finally, we can group our main functions together and run them once the DOM is fully loaded.

function init() {
        fetchData();
        handleConversionButtons();
        handleSortButton();
        handlePaginationButtons();
    }

document.addEventListener('DOMContentLoaded', init, false);

Demo

Weather App