Building Your First App With Vue.js
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.
This article has been updated and now works with Vue.js v2!
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:
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:
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: '#main' });
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.
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){ if(typeof resp.data == 'string') { resp.data = JSON.parse(resp.data); } 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 that goes through an uppercase filter (we will cover filters later in the article) 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'], methods: { getImageBackgroundCSS: function(img) { if(img && img!='self' && img!='nsfw') { return 'background-image: url(' + img + ')'; } else { return 'background-image: url(assets/img/placeholder.png)'; } } } });
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.
<template id="post"> <div class="post"> <a :href="item.data.url" :style="getImageBackgroundCSS(item.data.thumbnail)" 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 above template looks scary at first, but really isn't. We just take the properties of the post object and display them.
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 uppercase
filter we mentioned earlier in the subreddit template is one of the simplest filters possible. It was actually built-into previous version of Vue but was removed in v2 along with all other text filters.
It takes one string parameter, transforms it to uppercase and returns the result.
Vue.filter('uppercase', function(value) { return value.toUpperCase(); });
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.
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=5") .then(function(resp){ if(typeof resp.data == 'string') { resp.data = JSON.parse(resp.data); } this.posts=resp.data.data.children; }); } }); // Child | Componenet represiting a single post. var post = Vue.component('post', { template: "#post", props: ['item'], methods: { getImageBackgroundCSS: function(img) { if(img && img!='self' && img!='nsfw') { return 'background-image: url(' + img + ')'; } else { return 'background-image: url(assets/img/placeholder.png)'; } } } }); /*----------------- Custom filters -----------------*/ // Filter that transform text to uppercase. Vue.filter('uppercase', function(value) { return value.toUpperCase(); }); // 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) + '...'; } }); /*----------------- Initialize app -----------------*/ new Vue({ el: '#main' });
<!DOCTYPE html> <html> <head> <title>Vue</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> <header> <div class="header-limiter"> <h1><a href="https://blog.daitra.xyz/2016/08/building-your-first-app-with-vue-js/">Building Your First App With <span>Vue.js</span></a></h1> <nav> <a href="https://blog.daitra.xyz/2016/08/building-your-first-app-with-vue-js/">Download</a> </nav> </div> </header> <div id="main"> <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> </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="getImageBackgroundCSS(item.data.thumbnail)" 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> <!-- Demo ads. Please ignore and remove. --> <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="https://blog.daitra.xyz/misc/enhance/v3.js" async></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 :)
Bootstrap Studio
The revolutionary web design tool for creating responsive websites and apps.
Learn more
Comments 12
Can you help me please ? I tried everything I saw in comments, but no matter what i always get this type of error: https://i.imgur.com/EAdJCCg.png. Thanks in advance for help!
Great Tutorial. Thx.
VUE.js is a powerful tool to build dynamic webpages.
Do you know if Windows 10 can run the vue client? I try running vue run dev, and I don't get any errors, but it's not launching the app
If python is installed in Win Box, and python executable is included int the environment variable path, the aforementioned command:
python -m SimpleHTTPServer 8080
should work just fine, from the cmd.Then navigate to localhost:8080 should do the trick.
Could you please explain in more details, how your Subreddit Component gets connected to the Reddit API - step by step, understandable also for beginners. Where exactly you have found the commands/functions, that you use for connecting to the Reddit API and for extracting information from it.
If you look at the JavaScript code for the Subreddit Component, you will see a created() function. It gets called when a new instance of this component is created.
Now, see what's inside the created() function - we simply send a GET request to a specific reddit URL, and in a promise receive all the needed information about that subreddit.
There is nothing else to it, the whole connection happens in these 6 lines:
I had to change the following line as Chrome 55 was complaining for some reason: script.js:17 Uncaught (in promise) TypeError: Cannot read property 'children' of undefined at o.<anonymous> (script.js:17) - it wasn't able to understand the JSON it pulled down.
Hi pat, we noticed this issue as well.
The problem is that for some reason Chrome and Firefox return differently structured responses. To make it work in both browsers, it's better to add the following block:
This way we can make sure we are always working with a JSON.
Oh yeah, I also do check for string and then do json parse
Thanks for this!!
Hi Danny,
Vue.js is great, and you wrote a nice tutorial. Thanks for sharing.
A small improvement (an added flexibility) to the truncate filter below.
Note The SimpleHTTPServer module has been merged into http.server in Python 3. The 2to3 tool will automatically adapt imports when converting your sources to Python 3.