Hacker News Vue Top Stories Client
Introduction
Over the past few years, I've used AngularJS as my front-end framework for most web projects. With the release of Angular and React, I tried both of them and while both are great tools the learning took significantly longer because I also had to learn the bundler systems and in the case of Angular, TypeScript. Although the CLI tools go a long way towards automating a lot of the setup process, building an application from scratch in these frameworks is not intuitive especially for a newcomer. In the early days of Vue, I was able to attend a Meetup presentation by Evan You which went over the philosophy of Vue as well as its capabilities. I was instantly interested because of how easy it was to get started. Since that presentation, I have not had time to try it but with AngularJS being phased out, I felt it was about time to start exploring another framework. To get a better understanding of the framework's features prior to using CLI tools, I created a standalone application that relies only on NPM packages. In this writeup, I will create a Hacker News client with Vue that calls the Hacker News API and displays the top 50 stories. Source code for this project can be found in the following repository.
Prerequisites
Initialize Project
The first thing we want to do is create a directory for our project and initialize it with NPM
mkdir hnvuedemo
cd hnvuedemo
npm init -y
Install Dependencies
Next, we want to install our dependencies. (Express is optional)
npm install --save vue axios express
Create Server (Optional)
We can set up a server to host and serve our content. This is optional though because the application should still work when the index.html
file is opened in the browser.
In our root project directory, we can create the server.js
file and add the following content:
var express = require("express");
const PORT = 3000 || process.env.PORT;
var app = express();
app.use(express.static("."));
app.get("/", (req, res) => {
res.sendFile("index.html");
});
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
Create Application
Create The View
Now it's time to create our application. Let's start by scaffolding the index.html
which is where our application will be displayed.
<!DOCTYPE html>
<html>
<head>
<script src="./node_modules/vue/dist/vue.min.js"></script>
<script src="./node_modules/axios/dist/axios.min.js"></script>
</head>
<body>
<!--App Content Here-->
<!--App Scripts Here-->
</body>
</html>
We want to add the scripts to the Vue
and axios
packages in our head
element. We'll hold off on the content for now.
Get Data
Our data source for this project will be the Hacker News API. To interact with it, we'll be using the axios
NPM package. The data collection happens over several HTTP requests, therefore to help with it we'll be creating a file called apihelpers.js
in our root project directory that will contain functions to get and manipulate the data.
Get Top Stories
The first thing we want to do is get a list of top stories. We can do so via the /topstories
endpoint. The response of this request returns a list of ids which can then be used in conjunction with the /item/{id}
endpoint to get the individual story data. We can then validate the story object to make sure it is a story as opposed to a user or job posting and return a list of all the story objects. The functions that will help us with that are getIds
, isStory
and extractStories
.
/**
* Checks whether the item is a story
* @param {Object} story - Story object
*/
function isStory(story) {
return story.type == "story";
}
/**
* Gets ids of stories
* @param {string} url - Url to fetch story ids
*/
function getIds(url) {
return axios.get(url);
}
/**
*
* @param {Array<Object>} - List of resolved promises
*/
function extractStories(...responses) {
var stories = responses.map(story => story = story.data);
return stories.filter(isStory);
}
Create Vue App
Now that we have our helper methods to make calls to the API, we can create our application.
We can start by creating our Vue instance which is where our application will live. We can create a file called app.js
in our root project directory and add the following content:
//Components Go Here
new Vue({
el: "#app",
data: {
title: "Hacker News Vue Simple Reader",
loading: true,
topStories: []
},
methods: {
getTopStories: function() {
getIds(
"https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty"
)
.then(ids => buildRequest(ids.data.splice(0, 50))) //Only get first 50
.then(getStories)
.then(axios.spread(extractStories))
.then(stories => {
this.topStories = stories;
this.loading = false;
})
.catch(e => console.log(e));
}
},
created: function() {
this.getTopStories();
}
});
There's a lot happening here, so let's break it down based on the properties of the object passed to the Vue constructor.
The el
property is like a selector which tells the application which element it should operate on. The value #app
tells it to look for an element where the id
attribute is app
. This of course can be anything of your choosing.
The data
property specifies the properties that contain the data of our application. These properties are reactive which means that whenever they change, that change is reflected in our view. As such, even if they have no value when the application starts, in order for them to automatically change when data is passed to them, they need to be declared in the data
property. In our application, we have a title
property which will be the title of our web page, topStories
which is where we'll store the list of Hacker News stories and loading
which we'll use to let us know when data is being loaded into our application.
In the methods
property, we define functions that we want our application to use. In this case, I created the getTopStories
method which chains together all the functions defined in our apihelpers.js
file to return the top 50 stories on Hacker News.
Finally, Vue has instance lifecycle hooks. The created
property defines what should happen when our instance is created. In our case, we want to call the getTopProperties
method which is defined in our methods
property to load the data, update our topStories
data property and set loading
to false
because our data has loaded successfully.
Add App to View
Now that we have created the logic of our application, it's time to add it to our view. We can do so by adding the apihelpers.js
and app.js
files to our index.html
file via script
elements.
<!DOCTYPE html>
<html>
<head>
<script src="./node_modules/vue/dist/vue.min.js"></script>
<script src="./node_modules/axios/dist/axios.min.js"></script>
</head>
<body>
<!--App Content Here-->
<!--App Scripts Here-->
<script src="apihelpers.js"></script>
<script src="app.js"></script>
</body>
</html>
Display Data
Although we can now use our application logic inside of our view, we still can't see any of it because we have not added elements to display it. To view our application data, we can add the following code to our index.html
file below the <!--App Content Here-->
section.
<div id="app">
<h2>{{title}}</h2>
<h5 v-if="loading">Loading...</h5>
<story v-for="story in topStories" :key="story.id" :story="story"></story>
</div>
The final index.html
contents should look like the content below:
<!DOCTYPE html>
<html>
<head>
<script src="./node_modules/vue/dist/vue.min.js"></script>
<script src="./node_modules/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
<h5 v-if="loading">Loading...</h5>
<story v-for="story in topStories" :key="story.id" :story="story"></story>
</div>
<script src="apihelpers.js"></script>
<script src="app.js"></script>
</body>
</html>
As mentioned earlier, we created an element, in this case a div
that has the id
attribute with a value of app
. This is how our application knows where to display our content. Inside of an h2
element, we display our title
data property. Below it, we have an h5
element that displays the text "Loading...". However, this is to only be displayed when our loading
data property is true
. If not, it should not be visible. We can achieve this conditional rendering via the v-if
directive. This directive evaluates the expression inside of it and renders content based on its truthiness.
Finally, there's one last piece that looks like an element, but not one of the built-in HTML elements. So then, what is it? It's a component. The Vue website defines a component as "...a reusable Vue instance with a name: in this case, <story>
. We can use this component as a custom element inside a root Vue instance...". In our component, the v-for
directive is what it sounds like. It creates a sequence of story
components based on a list of objects defined in our Vue
instance's data
property. Our story
component iterates over the topStories
data property and assigns the value of the individual object in the list to the variable story
. We bind the id
property of the story
object to the key
attribute of the component and pass in the entire object to the component via the story
prop. A prop is a custom attribute that you can register for the component. We can use props to pass data into the component. In all cases, the :
prefix on the attributes and props of the component are shorthand for the v-bind
directive which dynamically binds an expression to an attribute or component prop.
With all that being said your next question might be, how does the view know about this component? The answer is it doesn't at least not until you define it which is what we'll do next in our app.js
file. In order for our story
component to be usable, we need to define it above the instantiation of our Vue
instance. The definition looks like the following:
Vue.component('story',{
props: ['story'],
template: `
<div>
<h3><a :href="story.url" target="_blank">{{story.title}}</a></h3>
</div>
`
});
Like our Vue
instance, let's unpack what's happening here. The first parameter is a string with the name of our component. The props
property is a list of the props or custom attributes that are accepted by our component. The template
property is where we set up the template of what will be rendered in place of our component in the view. In our case, we'll have an h3
element with a nested a
element whose href
attribute is the url
property of our story
object and the display text is the title
property of our story
object.
Run Application
At this point, our application should be ready to run. You can either start the server with the following command npm start
and navigate to http://localhost:3000
or open the index.html
page in the browser of your choice. The result should look like the screenshot below:
Conclusion
In this writeup, I built a standalone Hacker News client that displays the top 50 stories using Vue while also highlighting some of its main features. Overall, I really enjoyed building this application. The setup process was extremely simple and after a few hours looking through the excellent documentation and working through some bugs, it took less than two hours to get this application up and running from start to finish. For prototyping and learning purposes, Vue is great because you're able to take advantage of the core features of the framework without having too much overhead. Although this may not be the most appropriate way to build production-ready applications, it's nice to know you can have a modern web application with minimal setup required. My next steps will be to continue learning some of the other features the framework provides and eventually build up to learning how to use the CLI tools.