Learning never exhausts the mind

Published on by

Step by step guide to creating your first Vue.js app. We install and configure Vue.js, create a new project and build a portfolio website using Flickr API.

In this tutorial series, we'll create a simple portfolio style website using Vue.js, Axios, and the Flickr API. The goal of this series is to practice basic Vue concepts and gain exposure to working with an API. I am assuming a basic working knowledge of HTML, CSS, and Javascript.

The source files for this tutorial are available on GitHub at the end of the article.

Installing Vue.js CLI

Vue CLI requires Node.js version 8.9 or above. If you haven't got Node installed head over to the Node download page and follow the installation instructions to install Node.

To install the Vue.js package, enter the following command into PowerShell (or terminal) with administrator privileges.

npm install -g @vue/cli

After installation, you will have access to the vue binary in your command line. You can verify that it is properly installed by simply running vue, which should present you with a help message listing all available commands.

Project Setup

Now we get to create our project. Navigate to where you want to save (e.g. c:\dev\) it in your PowerShell and entering the following:

vue create portfolio

The PowerShell window will show a wizard style installer. Simply select the following options.

Take the default option of babel, eslint

Take the default option of babel, eslint

Vue create project can be slow. Be patient.

Vue create project can be slow. Be patient.

Once that it done it will create a folder called portfolio in c:\dev\ (or wherever you ran the command) and create a sample template.

CD into the new portfolio folder and run this command to confirm the project was successfully created.

npm run dev

If everything has worked successfully you will see the following in your browser:

First Vue.js Project Welcome Screen

First Vue.js Project Welcome Screen

Now in the PowerShell window, hit Ctrl+C to stop the server running. We need to install a few packages to get our app running.

First we will use axios to handle our AJAX requests to the Flickr API. Install the package by running this command in the PowerShell window.

npm install axios --save

Next we are going to use the moment package to format dates for us. Install moment using this command.

npm install moment --save

If all goes well you should see a successful message.

Flickr API Setup

Working with the Flickr API requires a key. This key is a unique identifier that lets Flickr know we're a legitimate source making a request. The key is free but you do have to have a Flickr account.

Head over to Flickr and log in (register for an account if you don't have one) then visit the Flick API Services page. Take a moment to read the guides under "Read these first" especially the terms and conditions, so you don't get banned.

Click the link for "API Keys" then "Create an App". Fill in the details and once validated you will get a Key and a secret.

In the root of the project directory, (at the same level that index.html is located) create a file called config.js and add the following contents:

export default {
  api_key: "YOUR_KEY_HERE"
}

The app I am going to build will display the images contained within a Flickr album. Simply browse to our Flickr albums and find the one you wish to display. In the URL bar copy out the album ID and add it to the config file.

Grab the Photoset ID from the URL bar of a Flickr Album

Grab the Photoset ID from the URL bar of a Flickr Album

export default {
  api_key: "YOUR_KEY_HERE",
  photoset: "72157700194273182"
}

Making Our First API Call

We now have all the pieces to fetch photos from Flickr. Empty out Home.vue and replace with this contents.

<template>
  <div class="home">
    
  </div>
</template>

<script>
export default {
  name: 'home',
};
</script>

Next, we are going to add some code to call the Flickr API using our key and show the results on the page. I'll explain each chunk of code and provide a full listing at the end which you can use to see how it fits together.

Firstly we need some markup to show our page properly.

<template>
  <div>
    <div class="wrapper" id="page">
      <p v-if="loading">
        Loading...
      </p>
      <ul v-else>
        <li v-for="image in images" :key="image.id">{{image}}</li>
      </ul>
    </div>
  </div>
</template>

Next, we need to import our config file and the axios package into the vue. This is done using the import command followed by the module being imported and then the file it is imported from.

import config from '../../config';
import axios from 'axios';

Next, I'm going to define some data variables. One to indicate if the page is busy (loading the contents) and another variable to hold the image data.

data() {
  return {
    loading: false,
    images: []
  }
},

Then we add two functions - one that sets the page view up and another which fetches the images from Flickr.

  methods: {
    loadImages() {
      this.loading = true;
      this.fetchImages()
        .then((response) => {
          this.images = response.data.photoset.photo;
          this.loading = false;
        })
        .catch((error) => {
          console.log("An error ocurred: ", error);
        })
    },
    fetchImages() {
      return axios({
        method: 'get',
        url: 'https://api.flickr.com/services/rest',
        params: {
          method: 'flickr.photosets.getPhotos',
          api_key: config.api_key,
          photoset_id: config.photoset,
          extras: 'url_n, url_o, owner_name, date_taken, views',
          page: 1,
          format: 'json',
          nojsoncallback: 1,
          per_page: 30,
        }
      })
    },
  },

The first method, loadImages, is going to set the loading varible to true. Through the magic of Vue data binding when the loading variable is true the v-if statement becomes true and the loading text is shown, else the list will be shown. v-if can be used on any element to conditionally show or hide. Next, this method is going to call our other method with a JavaScript promise. If it is successful it will put the images into our data container. If it fails the error will be logged in the console.

The second method uses Axios to query the Flickr API. It's fairly straightforward. You can see where we get the photoset id and API key from the config, it's going to call the flickr.photosets.getPhotos API method and return a few extra columns in the dataset.

The final thing we need to do is to get all this to show on page load.

beforeMount(){
  this.loadImages()
}

Putting all that together we get the full contents of Home.Vue.

<template>
  <div>
    <div class="wrapper" id="page">
      <site-header />
      <p v-if="loading">
        Loading...
      </p>
      <ul v-else>
        <li v-for="image in images" :key="image.id">{{image}}</li>
	  </ul>
    </div>
  </div>
</template>

<script>
import config from '../../config';
import axios from 'axios';

export default {
  name: 'home',
  components: { },
  data() {
    return {
      loading: false,
      images: []
    }
  },
  methods: {
    loadImages() {
      this.loading = true;
      this.fetchImages()
        .then((response) => {
          this.images = response.data.photoset.photo;
          this.loading = false;
        })
        .catch((error) => {
          console.log("An error ocurred: ", error);
        })
    },
    fetchImages() {
      return axios({
        method: 'get',
        url: 'https://api.flickr.com/services/rest',
        params: {
          method: 'flickr.photosets.getPhotos',
          api_key: config.api_key,
          photoset_id: config.photoset,
          extras: 'url_n, url_o, owner_name, date_taken, views',
          page: 1,
          format: 'json',
          nojsoncallback: 1,
          per_page: 30,
        }
      })
    },
  },
  beforeMount(){
    this.loadImages()
  }
};
</script>

Now back in the PowerShell window, you can run the application using npm run serve. If all goes well you should see an ugly screen showing a bunch of JSON data.

Ugly JSON output from Flickr API

Ugly JSON output from Flickr API

The next thing to do is tidy this up and create a component or two to display the image nicely.

Make a Vue.js Component

Components in Vue are tiny little self-contained modules which you can reuse in a project. Components can be reused as many times as you want, which is lucky since we are going to build one component and reuse it multiple times.

In your src folder, create a new folder called components. Within this folder create a new file called ImageCard.vue. Open this file in your editor.

The first thing we are going to add is the template markup.

<template>
  <div class="item">
    <a href="#">
      <img class="image" :src="image.url_n" :alt="image.title">
      <div class="body">
        <p v-if="image.title" class="image-title">{{image.title}}</p>
        <p v-else class="image-title">No Title Found</p>
        <p class="image-owner">By {{image.ownername}}</p>
        <section class="image-date-view-wrapper">
          <p class="image-date">{{image.datetaken}}</p>
          <p class="image-views">Views: {{image.views}}</p>
        </section>
      </div>
    </a>
  </div>
</template>

You may have noticed the colon before an attribute, for example, I used :alt="image.title". This just means that the attribute is data bound to the image objects title property. For outputting data as plain text we surround it in handlebars - {{image.title}}.

Next, we add the TypeScript code. Fairly simple here. We export a module with the name ImageCard and it has a property called image.

<script>
import config from '../../config';

export default {
  name: 'ImageCard',
  props: [ 'image' ]
}
</script>

And finally, some styling so it looks pretty. Putting it all together we get this.

<template>
  <div class="item">
    <a href="#">
        <img class="image" :src="image.url_n" :alt="image.title">
        <div class="body">
        <p v-if="image.title" class="image-title">{{image.title}}</p>
        <p v-else class="image-title">No Title Found</p>
        <p class="image-owner">By {{image.ownername}}</p>
        <section class="image-date-view-wrapper">
            <p class="image-date">{{image.datetaken }}</p>
            <p class="image-views">Views: {{image.views}}</p>
        </section>
        </div>
    </a>
  </div>
</template>

<script>
import config from '../../config';

export default {
  name: 'ImageCard',
  props: [ 'image' ]
}
</script>

<style lang="scss">
.item {
  background-color: #eee;
  display: inline-block;
  margin: 0 0 1em;
  width: 100%;
}
.item a {text-decoration:none}
.item:hover .body {
    visibility: visible;
    opacity: 1;
}
.image {
  width: 100%;
  height: auto;
  object-fit: cover;
}
.body {
  padding: .5rem 1rem 1rem;
  position:relative;
  height:95px;
  margin:-100px 0 0 0;
  background:rgba(0,0,0,0.5);
  color:white;
  visibility: hidden;
  opacity: 0;
  transition: visibility 0s, opacity 0.5s linear;
}
.image-title {
  font-weight: bold;
  margin: 0;
}
.image-owner {
  margin-top: 0;
  font-size: .8rem;
}
.image-title,
.image-owner {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}
.image-date-view-wrapper {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.image-date,
.image-views {
  margin-bottom: 0;
  font-size: .8rem;
}
</style>

Now to use this component in our app we need to make a few changes to the Home.vue file.

First, we need to import the component.

import ImageCard from '@/components/ImageCard';

Next, in the components array, we need to include ImageCard so that it is available to use.

components: {
  ImageCard
},

Next, we change the template markup to make use of the component. Instead of a list of raw image data, change it to this.

<p v-if="loading" class="text-centered">
  Loading...
</p>
<div v-else class="masonry">
  <image-card v-for="image in images" :key="image.id" :image="image" />
</div>

The property :image we created in the component will be data bound and contain the details from the image item in the loop. The loop is going to repeat the component passing in each image to the component in turn. Finally, we add some styling and you should have this as your Home.vue.

<template>
  <div>
    <div class="wrapper" id="page">
      <p v-if="loading" class="text-centered">
        Loading...
      </p>
      <div v-else class="masonry">
        <image-card v-for="image in images" :key="image.id" :image="image" />
      </div>
    </div>
  </div>
</template>

<script>
import config from '../../config';
import axios from 'axios';
import ImageCard from '@/components/ImageCard';

export default {
  name: 'home',
  components: {
    ImageCard
  },
  data() {
    return {
      loading: false,
      images: []
    }
  },
  methods: {
    loadImages() {
      this.loading = true;
      this.fetchImages()
        .then((response) => {
          this.images = response.data.photoset.photo;
          this.loading = false;
        })
        .catch((error) => {
          console.log("An error ocurred: ", error);
        })
    },
    fetchImages() {
      return axios({
        method: 'get',
        url: 'https://api.flickr.com/services/rest',
        params: {
          method: 'flickr.photosets.getPhotos',
          api_key: config.api_key,
          photoset_id: config.photoset,
          extras: 'url_n, url_o, owner_name, date_taken, views',
          page: 1,
          format: 'json',
          nojsoncallback: 1,
          per_page: 30,
        }
      })
    },
  },
  beforeMount(){
    this.loadImages()
  }
};
</script>

<style lang="scss">
.text-centered {
  text-align: center;
}
.wrapper {
  margin: 0 auto;
  max-width: 1200px;
  @media only screen and (max-width: 799px) {
    max-width: 100%;
    margin: 0 1.5rem;
  }
}
.masonry {
  margin: 1.5em auto;
  max-width: 1200px;
  column-gap: 1.5em;
}

@media only screen and (min-width: 1024px) {
  .masonry {
    column-count: 3;
  }
}

/* Masonry on medium-sized screens */
@media only screen and (max-width: 1023px) and (min-width: 768px) {
  .masonry {
    column-count: 2;
  }
}

/* Masonry on small screens */
@media only screen and (max-width: 767px) and (min-width: 540px) {
  .masonry {
    column-count: 1;
  }
}
</style>

You should now have something like this in your browser. Your images will vary depending on the photoset you use.

Basic Vue Potfolio Website

Basic Vue Potfolio Website

Tiding Up Our App

There are a few things we can do to tidy up the app.

Sometimes broken images come through from the API, so we can easily filter them out so as not to break the page.

This is done by adding a computed section at the end of the class.

...
  beforeMount(){
    this.loadImages()
  },
  computed: {
    cleanImages() {
      return this.images.filter(image => image.url_n)
    }
  },
};

We then just need to adjust the for loop to look at the filtered items.

<image-card v-for="image in cleanImages" :key="image.id" :image="image" />

Next in the ImageCard.vue we can tidy up the dates using the moment library. This is done by creating a filter which will return formatted values.

export default {
  name: 'ImageCard',
  props: [ 'image' ],
  filters: {
    moment(date) {
      return moment(date).format("Do MMMM YYYY");
    }
  }
}

In the template simply make a quick change to use this filter. This will pass the value of image.datetaken to the function moment and output the result.

<p class="image-date">{{image.datetaken}}</p>

Becomes

<p class="image-date">{{image.datetaken | moment}}</p>

I also added a header to the page by creating a new component called SiteHeader.vue with the following contents.

<template>
    <div>
        <header>
            <a href="#"><h1>My Photography Portfolio</h1></a> 
            <div class="menu">
                <ul>
                    <li><a href="#">Home</a></li>
                    <li><a href="#">About</a></li>
                    <li><a href="#">Contact</a></li>
                    <li><a href="#">Blog</a></li>
                    <li><a href="#">Twitter</a></li>
                </ul>
            </div>
        </header>
        <h2 class="text-centered">Hi. I'm a Photographer</h2>
        <p class="text-centered">You can add a catchy tagline or introduction here if you wish.</p>
    </div>
</template>

<script>
export default {
  name: 'SiteHeader'
}
</script>

<style lang="scss">
    header {
        margin:1em 0 0 0;
    }
    header::after{
        content: "";
        clear: both;
        display: table;
    }
    h1 {
        width: 190px;
        height: 72px;
        background: url("../assets/logo.png") top right;
        text-indent: -9999px;
        margin: 0 auto;
    }    
    .menu ul {
        margin:1em 0 0 0;
        padding:0;
    }
    .menu li {
        width:100%;
        padding:0.25em;
        display:block;
        list-style:none;
        margin:0;
        text-align:center;
    }
    .menu li a {
        color:#777;
        text-decoration:none;
        text-transform:uppercase;
    }
    h2 {
        margin:2em auto 1em auto;
    }
    p.text-centered {
        color:#777;
        margin-bottom:3em;
    }

    @media only screen and (min-width: 1024px) {
        h1 {
            margin:0;
            float:left;
        }
        .menu {
            float:right;
        }
        .menu li {
            display:inline;
        }
    }
</style>

This is included in the Home.vue by adding an imports and markup tag.

import SiteHeader from '@/components/SiteHeader';
components: {
  SiteHeader,
  ImageCard
},
<template>
  <div>
    <div class="wrapper" id="page">
      <site-header />
      <p v-if="loading" class="text-centered">

You should now have a nice pretty looking portfolio website driven from a Flickr album using the Flickr API.

Building a Vue.js Website with Axios and Flickr API

Building a Vue.js Website with Axios and Flickr API

Conclusions and Download Sample Project

This has been a very quick introduction to Vue.js, how to install Vue.js, create a new project, using Axios to query the Flick API. We've seen how to build a component and use filters over a data set.

You can download the full project source files using the link below.

Download from GitHub

Did you find this tutorial helpful? Was there anything missing or something you got stuck on? Let me know in the comments below and I'll do my best to fill in the gaps.

Leave a Reply

Fields marked with * are mandatory.

We respect your privacy, and will not make your email public. Hashed email address may be checked against Gravatar service to retrieve avatars. This site uses Akismet to reduce spam. Learn how your comment data is processed.