Building Your First App With Vue.js

building-your-first-app-with-vuejs

Today we’re going to exercise our Vue.js skills by building a simple app for browsing reddit posts. We’re going to construct the whole thing from scratch to demonstrate just how easy it is to create user interfaces with a framework like Vue.

This tutorial requires you to have at least some basic knowledge of JavaScript and Vue.js. If you aren’t familiar with Vue.js at all, we advise you to go and check out our article 5 Practical Examples For Learning Vue.js, where we show many of the core concepts with practical code snippets.

The App

What we want from our application is simply to fetch the feed from a number of subbreddits and display them. Here is what the end result will look like:

vue-app

Our Vue.js App

We will have six separate subreddit feeds showing five posts each. The posts have links to the content and discussion on reddit, as well as some other details. For the sake of simplicity we have omitted features such as adding/removing subreddits and doing searches, but they can be easily added on top of the existing app.

Setting Up The Workspace

You can download the full source code for the reddit browser app from the Download button near the top of the article. Before we actually look at the code, let’s make sure that everything is setup properly. Here is an overview of the file structure:

Our project's folder

Our project’s folder

As you can see it’s quite basic: we just have one HTML file, one CSS file, a script.js containing our JavaScript code. We’ve also added local copies of the Vue.js and Vue-resource libraries, but you can use a CDN if you prefer.

Thankfully, Vue.js doesn’t require any special configuration, so it should work straight out of the box. To start the app we just have to create a global Vue instance:

new Vue({
    el: 'body'
});

The only thing left to do now is start a local web server to enable cross-origin AJAX requests to the reddit API. The easiest way to do this on OS X/Ubuntu is by running the following command from the project’s directory:

python -m SimpleHTTPServer 8080

If everything is done properly our project should be available at localhost:8080.

Creating Custom Components

Our app is going to need two reusable components – one for the Posts, and another for Subreddits. The two components will be in a Child-Parent relationship, meaning that the Subreddit component will have multiple Posts nested in it.

Components Structure

Components Hierarchy

Let’s start with the Subreddit component, and more specifically it’s JavaScript:

// Parent | Subreddit component containing a list of 'post' components. 
var subreddit = Vue.component('subreddit',{
    template: '#subreddit',
    props: ['name'],

    data: function () {
        return { posts: [] }
    },

    created: function(){
        this.$http.get("https://www.reddit.com/r/"+ this.name +"/top.json?limit=5")
        .then(function(resp){
            this.posts=resp.data.data.children;
        });
    }
});

Here we define the new component under the name subreddit. In props we provide an array with all the parameters our component can receive – in this case it is just the name of the subbreddit we want to browse. Now if we want to add a subreddit block to the HTML we will use this markup:

<subreddit name="food"></subreddit>

The data property defines what variables are needed for each instance of the component and their default values. We will start with an empty posts array, and populate it in the created method. When a <subreddit> tag is created, Vue will take its name property, make a call to the reddit API to fetch the top 5 posts from the subreddit with that name, and save them in this.posts. For the HTTP requests we’ve used the vue-resource library instead of jQuery, since it is way tinier and automatically binds the correct context for this.

After we’ve acquired everything we need in the model, Vue.js will automatically render our Subreddit components. The actual view that the user sees is defined in a template in index.html:

<template id="subreddit">

    <div class="subreddit">
        <h2>{{ name | uppercase }}</h2>

        <ul class="item-list">
            <li v-for="obj in posts">
                <post :item="obj"></post>
            </li>
        </ul>
    </div>

</template>

Personally, I like to wrap all the elements of a component in a div container. This makes them easier to style and also seems more semantic (to me at least). Inside that container we have a title (the uppercase filter comes built-in with Vue) and an unordered list iterating over the elements returned from the reddit API call.

If you look closely at the HTML, you’ll also notice we are using a <post> tag. This isn’t some new fancy HTML element – it’s our child component!

// Child | Componenet represiting a single post.
var post = Vue.component('post', {
    template: "#post",
    props: ['item']
});

Post components will expect a object called item containing all of the information about a single post on reddit – things like title, URLs, number of comments, etc. As we saw earlier, this is done in a v-for loop inside the Subreddit (parent) component:

<li v-for="obj in posts">
    <post :item="obj"></post>
</li>

The colon prefixing :item="obj" is very important. It tells Vue that we are proving a JavaScript object called obj (as opposed to the string "obj"), allowing us to pass the data from the v-for.

Now that we have all the needed properties for a post, we can display them. The template looks scary at first, but really isn’t:

<template id="post">

    <div class="post">
        <a   :href="item.data.url" :style="item.data.thumbnail | setAsBackground" 
             target="_blank" class="thumbnail"></a>

        <div class="details">
            <a :href="item.data.url" :title="item.data.title" target="_blank" class="title">
                {{ item.data.title | truncate}}
            </a>          
            
            <div class="action-buttons">
                <a href="http://reddit.com{{ item.data.permalink }}" title="Vote">
                    <i class="material-icons">thumbs_up_down</i>
                    {{item.data.score}}
                </a>

                <a href="http://reddit.com{{ item.data.permalink }}" title="Go to discussion">
                    <i class="material-icons">forum</i>
                    {{item.data.num_comments}}
                </a>
            </div>
        </div>
    </div>
    
</template>

The only thing worth mentioning here is the use of the setAsBackground and truncate filters. In contrast to the uppercase filter we used earlier, they don’t come bundled with Vue and we had to make them ourselves.

Creating Custom Filters

Defining filters is quite easy. The Vue.filter() method provides us with the incoming string data, which we can transform whatever way we want and then simply return.

The first filter we’ll need takes the preview image for a post and creates a CSS rule setting it as background. We use this to set the inline styles with less effort.

It takes one parameter: the URL for the image. If that’s not available a placeholder image is shown instead.

// Filter that takes an image url and creates a CSS style.
Vue.filter('setAsBackground', function(value) {
    if(value && value!='self' && value!='nsfw') {
        return 'background-image: url(' + value + ')';  
    }
    else {
        return 'background-image: url(assets/img/placeholder.png)';   
    }
});

Our other filter takes strings and truncates them if they are too long. This is applied to the post titles, which often are way too lengthy for the design we had in mind.

// Filter for cutting off strings that are too long.
Vue.filter('truncate', function(value) {
    var length = 60;

    if(value.length <= length) {
        return value;
    }
    else {
        return value.substring(0, length) + '...';            
    }
});

The Full Code

Below we’ve listed all of the files for the app, so that you can look through the full code and get a better idea how the whole thing works.

/*-----------------
    Components 
-----------------*/

// Parent | Subreddit component containing a list of 'post' components. 
var subreddit = Vue.component('subreddit',{
    template: '#subreddit',
    props: ['name'],

    data: function () {
        return { posts: [] }
    },

    created: function(){
        this.$http.get("https://www.reddit.com/r/"+ this.name +"/top.json?limit=3")
        .then(function(resp){
            this.posts=resp.data.data.children;
        });
    }
});


// Child | Componenet represiting a single post.
var post = Vue.component('post', {
    template: "#post",
    props: ['item']
});


/*-----------------
   Custom filters 
-----------------*/

// Filter for cutting off strings that are too long.
Vue.filter('truncate', function(value) {
    var length = 60;

    if(value.length <= length) {
        return value;
    }
    else {
        return value.substring(0, length) + '...';            
    }
});


// Filter that takes an image url and creates a CSS style.
Vue.filter('setAsBackground', function(value) {
    if(value && value!='self' && value!='nsfw') {
        return 'background-image: url(' + value + ')';  
    }
    else {
        return 'background-image: url(assets/img/placeholder.png)';   
    }
});


/*-----------------
   Initialize app 
-----------------*/

new Vue({
    el: 'body'
});
<!DOCTYPE html>
<html>
<head>
    <title>Your First App With Vue.js</title>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="assets/css/styles.css">
</head>
<body>
    <div class="container">
        <subreddit name="aww"></subreddit>
        <subreddit name="space"></subreddit>
        <subreddit name="gifs"></subreddit>
        <subreddit name="food"></subreddit>
        <subreddit name="comics"></subreddit>
        <subreddit name="sports"></subreddit>
    </div>


    <template id="subreddit">
        <div class="subreddit">
            <h2>{{ name | uppercase }}</h2>
            <ul class="item-list">
                <li v-for="obj in posts">
                    <post :item="obj"></post>
                </li>
            </ul>
        </div>
    </template>

    <template id="post">
        <div class="post">
            <a   :href="item.data.url" :style="item.data.thumbnail | setAsBackground" 
                 target="_blank" class="thumbnail"></a>
            <div class="details">
                <a :href="item.data.url" :title="item.data.title" target="_blank" class="title">
                    {{ item.data.title | truncate}}
                </a>          
                
                <div class="action-buttons">
                    <a href="http://reddit.com{{ item.data.permalink }}" title="Vote">
                        <i class="material-icons">thumbs_up_down</i>
                        {{item.data.score}}
                    </a>

                    <a href="http://reddit.com{{ item.data.permalink }}" title="Go to discussion">
                        <i class="material-icons">forum</i>
                        {{item.data.num_comments}}
                    </a>
                </div>
            </div>
        </div>       
    </template>

    <script src="assets/js/vue.js"></script>
    <script src="assets/js/vue-resource.min.js"></script>
    <script src="assets/js/script.js"></script>
</body>
</html>
*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

a{
    text-decoration: none;
}

a:hover{
    text-decoration: underline;
}

html{
    font: normal 16px sans-serif;
    color: #333;
    background-color: #f9f9f9;
}

.container{
    padding: 27px 20px;
    margin: 30px auto 50px;
    max-width: 1250px;
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    background-color: #fff;
    box-shadow: 0 0 1px #ccc;
}

/* Subreddit component */

.subreddit{
    flex: 0 0 33%;
    min-width: 400px;
    padding: 20px 42px;
}

.subreddit h2{
    font-size: 18px;
    margin-bottom: 10px;
}

.subreddit .item-list{
    border-top: 1px solid #bec9d0;
    padding-top: 20px;
    list-style: none;
}

.subreddit .item-list li{
    margin-bottom: 17px;
}

/* Post component */

.post{
    display: flex;
}

.post .thumbnail{
    display: block;
    flex: 0 0 60px;
    height: 60px;
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
    margin-right: 10px;
    border-radius: 4px;
    margin-right: 12px;
}

.post .details{
    display: flex;
    flex-direction: column;
}

.post .details .title{
    font-size: 15px;
    margin-bottom: 3px;
    color: #04477b;
}

.post .details .title:visited{
    color: purple;
}

.post .details .action-buttons a{
    font-size: 11px;
    margin-right: 4px;
    display: inline-block;
    color: #666;
}

.post .details .action-buttons i{
    font-size: 10px;
    margin-right: 1px;
}

@media(max-width: 1250px){

    .container{
        justify-content: center;
        margin: 30px 30px 50px 30px;
    }
}

@media(max-width: 500px){

    .subreddit{
        min-width: 300px;
        padding: 20px 15px;
    }
}

Note that after creating our two components, the entire app interface comes down to:

<div class="container">
    <subreddit name="aww"></subreddit>
    <subreddit name="space"></subreddit>
    <subreddit name="gifs"></subreddit>
    <subreddit name="food"></subreddit>
    <subreddit name="comics"></subreddit>
    <subreddit name="sports"></subreddit>
</div>

The JavaScript file isn’t too large either and this is one of my favorite things about Vue. It does so much of the work for us that in the end we are left with a very clean and comprehensive piece of code.

Further Reading

The main focus of this tutorial was to show the process of building a simple Vue.js app. To keep it short we haven’t stopped to explain every tiny syntax peculiarity, but worry not! There are many awesome resources where you can learn the basics:

  • The official Vue.js starting guide and docs – here.
  • Excellent video series from Laracasts – here.
  • Our very own article: 5 Practical Examples For Learning Vue.js – here.

This concludes our Vue.js tutorial! We hope that you’ve had lots of fun with it and that you’ve learned a thing or two. If you have any suggestions or questions, feel free to leave a message in the comment section below :)

Powered by Gewgley