Create a JavaScript news app

Last updated: October 21, 2021.

In this tutorial, we will create a live news app using HTML, CSS and vanilla JavaScript. Data is imported from the Guardain’s OpenPlatform using JavaScript’s fetch command.

The application will display the latest news headlines with thumbnails and trail text. Upon a click, a user is able to access a full story and then return to headlines.

A feature of the app is that navigation from headlines to stories and back again is lightning fast: data is loaded by the initial fetch request when the app is loaded. After this, only images and videos for full stories clicked on by the user must be loaded.

What we will build

Below is a functional preview of the finished app:

See the Pen Untitled by James (@openjavascriptadmin) on CodePen.

Building the app

Step 1: HTML template and initial CSS

Let’s start with our HTML. This will be minimal because we will be controlling the behaviour of the web page using JavaScript.

The first element in our template is a header element with the id of header and a h1 title contained inside.

Then we have two elements for our two views: the div with the id of stories-container will be our headlines view. The story-container div, on the other hand, is our full story view.

Since we only want to see headlines when we first load the app, the story-container div (full story view) has a class attribute of no-display at first. We will manipulate this to be visible upon a user click later using JavaScript.

    <header id="header">
        <h1>&#127760; World News</h1>
    </header>
    <div id="stories-container"></div>
    <div id="story-container" class="no-display"></div>

We start the project with the following basic CSS styling:

        body {
            background: lightgray;
            height: 100%;
            margin: 0px;
            font-family: Arial, Helvetica, sans-serif;
        }
        header {
            text-align: center;
        }
        .no-display {
            display: none !important; /* used to hide an element */
        }

Step 2: Fetching news data

When our app first loads, we want to get the latest news data from the Guardain’s OpenPlatform. We do this using JavaScript’s fetch call (if you are unfamiliar fetch, you may want to read our guide to using fetch before continuing).

What we want to do is take the result, parse it to a JavaScript object and then pass the result of this to a new function for further data handling. So pass the result to handleRes(). For debugging, we also add .catch to the end of the promise chain to alert us to a fetch error:

        fetch('https://content.guardianapis.com/search?section=world&show-fields=all&show-blocks=body&api-key=test')
        .then(res => res.json())
        .then(res => handleRes(res))
        .catch(() => console.log("Fetch failed"));

Our fetch request is customised to get the following data from the API: news stories from the ‘world’ section, meta-information associated with the content (show-fields=all) and full story text(show-blocks=body).

Note that you can customise the request to get stories from a different section and include different types of story information in your app. See the API documentation for valid request terms.

Step 3: Building the headlines view

To build the headlines view, we need to extract the relevant information from the fetched data. To do this, we want to iterate through the returned stories and print them within their own article elements.

Because a JavaScript object is not iterable, we use Object.entries() in the category where the stories are nested (data.response.results) to turn the stories into an iterable array.

We then use the map method with a template literal inside to create the template with injected content for each story (title, thumbnail, trail text).

We then append this template for each story inside an article element and also create and append a button that will go to the full story view when click. We also save data that we will need in the full story view as attributes on the article element so it is available to us there without making another fetch request. Finally, we append each article within the stories-container div in our original HTML template (we make this conditional if the story being of type article to eliminate blogs and other content).

To complete the headlines view, we add a button to the header element that can be used to refresh the headlines. This button simply refreshes the page by running location.reload() upon a click (i.e. restarts the app and makes a new fetch request).

The code for this functionality is printed below:

function handleRes(data) {
            // Access fetched data as array
            let dataArray = Object.entries(data.response.results);

            // Create each article using map
            let article, articleContent;
            dataArray.map((story) => {

                // Construct content of each with template literal              
                articleContent = `
                <h3>${story[1].webTitle}</h3>
                <img src="${story[1].fields.thumbnail}">
                <p>${story[1].fields.trailText}</p>
                `

                // Create article element (container)
                article = document.createElement('article');
                // Set innerHTML of article element to template
                article.innerHTML = articleContent;
                // Create and append button to show full story
                button = document.createElement('button');
                button.innerText = "Read full story";
                button.id = "showStory";
                button.addEventListener('click', showFullStory);
                article.append(button);
                // Save data for full story view in element attributes
                article.setAttribute('title', story[1].webTitle);
                article.setAttribute('body', story[1].blocks.body[0].bodyHtml);
                article.setAttribute('img', story[1].fields.main);
                article.setAttribute('author', story[1].fields.byline);

                // append article so it appears in headlines view
                // IF it is an article
                if (story[1].type == "article") {
                storiesContainer.append(article);
                }
           })

           // Generate button to refresh headlines and
           // append to header element
           button = document.createElement('button');
           button.id = "refresh";
           button.innerText = "Refresh headlines";
           button.addEventListener('click', () => { location.reload(); });
           header.append(button);
        }

To make this appear in the same way as the live preview at the beginning of this tutorial, add the following CSS styling. This displays the articles in a horizontal flex-box format and with wrapping to a new line when necessary.

#stories-container {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
        }
        #stories-container article {
            display: inline-block;
            width: 16rem;
            margin: 1rem;
            padding: 1.5rem;
            background-color: rgb(250, 250, 250);
            border: lightgray 0.1rem solid;
            border-radius: 0.5rem;
            box-shadow: 0.1rem 0.05rem 0.05rem lightgray;
        }
        #stories-container article h3 {
            margin-top: 0px;
        }
        #stories-container article img {
            width: 16rem;
            margin-bottom: 1rem;
        }
        .no-display {
            display: none !important;
        }
        #refresh {
            display: block;
            margin: 1rem auto;
            font-weight: 400;
            color: #212529;
            text-align: center;
            border: 1px solid transparent;
            padding: .375rem .75rem;
            font-size: 1rem;
            line-height: 1.5;
            border-radius: .25rem;
            color: #fff;
            background-color: #5cb85c;
        }
        #showStory {
            display: block;
            margin: 1rem auto;
            font-weight: 400;
            color: #212529;
            text-align: center;
            border: 1px solid transparent;
            padding: .375rem .75rem;
            font-size: 1rem;
            line-height: 1.5;
            border-radius: .25rem;
            color: #fff;
            background-color: #007bff;
        }

Step 4: Building the full story view

The full story view is constructed in a similar way to the headlines view.

But first, at the beginning of the function, it is necessary to make the headlines view invisible. For this we display add the no-display class to the stories-container div in which we appended all of the stories content and also the refresh button.

We then remove no-display from story-container div in our initial HTML document. We can now place the full story content in this div and it will appear in the same place as the (now invisible) headlines did.

Similarly to the last step, we create a template string and place content inside it (no map is necessary this time as we are only appending one and not multiple stories).

Importantly, the data this time comes from the attributes of the clicked article (using the e.target.parentElement.getAttribute() syntax) and not directly from the fetched data (which is no longer available). Because the data is already saved, this will make switching to the full page view almost instantaneous!

Finally, we create a “return to headlines” button and append this in the header element. Clicking this triggers the headlines view (which still exists but is invisible) to return and clear the story view. It is better to clear this view than make it invisible because it can easily be recreated for another story using the data available from the headlines view (stored in attributes of the article elements).

        function showFullStory(e) {
            // Make the headlines view invisible
            storiesContainer.classList.add("no-display");
            // Make the full story view visible
            storyContainer.classList.remove("no-display");
            // Make the refresh button invisible
            document.getElementById('refresh').classList.add("no-display");

            // Create the HTML for the full story view
            // using the data saved as attributes
            storyContainer.innerHTML = `
            <h2>${e.target.parentElement.getAttribute('title')}</h2>
            <span class="author">By ${e.target.parentElement.getAttribute('author')}</span>
            <article>
                ${e.target.parentElement.getAttribute('img')}
                ${e.target.parentElement.getAttribute('body')}
            </article>
            `

            // Makes sure user starts from top of story
            window.scrollTo(0,0); 

            // Create a return button to return to headlines view
            let returnButton = document.createElement('button');
            returnButton.innerText="Return to headlines";
            returnButton.id="return";

            // Clear story and make headlines appear again
            returnButton.addEventListener('click', () => {
                // Clear storyContainer and make invisible
                storyContainer.innerHTML = "";
                storyContainer.classList.add("no-display");
                // Make headlines view visible again
                storiesContainer.classList.remove("no-display");
                // Make the refresh button visible again
                document.getElementById('refresh').classList.remove("no-display");
                // Remove the return button
                document.getElementById('return').remove();
            })

            // Append return button to header
            header.append(returnButton);
        }

To style the story view as it appears in the live preview, add the following CSS:

        #return {
            display: block;
            margin: 1rem auto;
            font-weight: 400;
            color: #212529;
            text-align: center;
            border: 1px solid transparent;
            padding: .375rem .75rem;
            font-size: 1rem;
            line-height: 1.5;
            border-radius: .25rem;
            color: #fff;
            background-color: #007bff;
        }

        #story-container {
            background-color:rgb(245, 245, 245);
            margin: 0px auto;
            min-width: 80vw;
            max-width: 800px;
            padding: 2rem;
            padding-top: 2rem;
            border-radius: 0.5rem;
            min-width: 200px;
        }
        #story-container h2 {
            font-size: 1.5rem;
        }
        #story-container .author {
            margin-top: 2rem;
            margin-bottom: 2rem;
        }
        #story-container img, #story-container figure, #story-container .element {
            display: block;
            margin: 2rem auto;
            width: 80%;
            height: auto;
            text-align: center;
        }
        #story-container iframe {
            display: block;
            margin: 2rem auto;
            width: 30rem;
            height: 20rem;
            text-align: center;
        }

Summary

In this tutorial, we have seen how we can build a news app using JavaScript’s fetch import data from a third-party API.

This is just one example of what can be built using Guardain’s OpenPlatform API and APIs in general.

Got the itch to build more? Check out this list of free APIs at public-apis.io!