Skip to content
Brandon der Blätter edited this page Feb 23, 2018 · 5 revisions

Lab 3 Tutorial

This tutorial will walk you through Lab 3, which builds a website for browsing the xkcd comic.

Loading data from an API

The first step is to load data from the XKCD API. We've done this with jQuery before, but this time we're going to use the JavaScript fetch API. This is relatively new (about 2015), and offers similar features as jQuery, so we don't have to load an additional library. I recommend this blog article by David Walsh for an introduction on how it works.

One complication is that the official XKCD API does not use CORS, so this will result in a CORS error. To avoid this, we'll use an unofficial, but CORS-enabled XKCD API.

The first step is to modify script.js to setup our Vue instance and fetch the latest XKCD comic:

var app = new Vue({
  el: '#app',
  data: {
    number: '',
    current: {},
  },
  created: function() {
    this.xkcd();
  },
  methods: {
    xkcd: function() {
      fetch('https://xkcd.now.sh/' + this.number).then(response => {
	return response.json();
      }).then(json => {
	this.current = json;
	return true;
      }).catch(err => {
      });
    },
  }
});

Most of this you should be familiar with from the Vue.js guide. We create an instance of Vue, tell it to control the DOM inside the tag with the #app CSS id, and setup some variables.

We also create a method, called xkcd to fetch the comic. This method uses the fetch API, JavaScript promises, and the new function syntax in ES6. To learn about promises, see this post on asynchronous JavaScript, up to and including the section on JavaScript promises. To learn about the new function syntax, see this ES6 tutorial.

The important thing to know here is that fetch returns a Promise, which gets resolved when the fetch completes. When it does, the code you place in then gets executed. This is what it means to have your JavaScript execute asynchronously. Anything placed after the fetch statement (after the final semicolon) is executed immediately after the call to fetch. The code inside then is executed only after the resource is actually fetched.

Once the fetch returns, we execute a function inside of then. This function takes one argument, response, and calls response.json() to convert it to JSON. Functions in a Promise chain must always return. The value that is returned is the parameters for the next then statement. Notice the second then also calls a function, which takes one argument, json, and uses this to update the value of current. This is called Promise chaining.

Here's an example of what the JSON looks like that is returned from the API:

{"month":"2","num":1958,"link":"","year":"2018","news":"","safe_title":"Self-Driving Issues","transcript":"","alt":"If most people turn into muderers all of a sudden, we'll need to push out a firmware update or something.","img":"https://imgs.xkcd.com/comics/self_driving_issues.png","title":"Self-Driving Issues","day":"21","imgRetina":"https://imgs.xkcd.com/comics/self_driving_issues_2x.png"}

To use this fetched data, modify index.html to insert the following after the h1 tag:

      <img v-bind:src="current.img" v-bind:alt="current.alt">

This will bind the src and alt attributes to the appropriate pieces of the JSON that was returned.

Waiting to load content

When you load data asynchronously, it is a good idea to display a message to the user in case this takes a long time. IN this step, we'll modify the code to do this.

First, in script.js, add a new loading variable to the data in the Vue instance. It should look like this:

  data: {
    number: '',
    current: {},
    loading: true,
  },

Second, modify the xkcd function to set and clear this variable:

    xkcd: function() {
      this.loading = true;
      fetch('https://xkcd.now.sh/' + this.number).then(response => {
	return response.json();
      }).then(json => {
	this.current = json;
	this.loading = false;
	return true;
      }).catch(err => {
      });
    },

Finally, modify index.html so that it uses this variable to show and hide a message:

      <div v-if="loading">
	<p>Loading...</p>
      </div>
      <div v-else>
	<img v-bind:src="current.img" v-bind:alt="current.alt">
      </div>

You should now be able to (briefly) see the loading message as the comic is loaded.

Showing additional information about the comic

We should show some additional information about the comic. We can do this just inside of index.html:

	<h2>{{ current.safe_title}}</h2>
	<img v-bind:src="current.img" v-bind:alt="current.alt">
	<p><i># {{number}}, drawn on {{current.day}} {{month}} {{current.year}}</i></p>

We're also going to make a small change to the xkcd method in script.js, to keep track of the current comic number:

    xkcd: function() {
      this.loading = true;
      fetch('https://xkcd.now.sh/' + this.number).then(response => {
	return response.json();
      }).then(json => {
	this.current = json;
	this.loading = false;
	this.number = json.num;
	return true;
      }).catch(err => {
      });

Since we start with number being an empty string, this lets us set number to the current comic number once it is known.

We also want to compute a the name of the current month, since it is only given to us in numeric form. For this, we can use a computed property in script.js:

  computed: {
    month: function() {
      var month = new Array;
      if (this.current.month === undefined)
	return '';
      month[0] = "January";
      month[1] = "February";
      month[2] = "March";
      month[3] = "April";
      month[4] = "May";
      month[5] = "June";
      month[6] = "July";
      month[7] = "August";
      month[8] = "September";
      month[9] = "October";
      month[10] = "November";
      month[11] = "December";
      return month[this.current.month - 1];
    }
  },

This property simply translates the numeric representation of the month to a string representation.

Your code should now show the comic title, number, and date.

Navigating with buttons

We would like to be able to navigate to comics written on other days. To do this, add some buttons to index.html:

      <p>
	<button v-on:click="previousComic">Previous</button>
	<button v-on:click="nextComic">Next</button>
      </p>

To make these buttons work, add these methods to script.js:

    previousComic: function() {
      this.number = this.current.num - 1;
    },
    nextComic: function() {
      this.number = this.current.num + 1;
    },

Note how simple the previousComic and nextComic methods are. They just need to update number. However, we need to tell Vue to fetch the new comic whenever the number changes. To do this, add a watcher in script.js, right after the computed properties:

  watch: {
    number: function(value,oldvalue) {
      this.xkcd();
    },
  },

The watcher is called every time the value of number changes.

You should now have working navigation buttons.

Navigating to a random comic

We'll now add another button to navigate to a random comic. To do this, add the button after the other buttons in index.html:

      <p>
	<button v-on:click="randomComic">Random</button>
      </p>

This calls a method when clicked, and we need to add that method to script.js:

    getRandom: function(min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum and minimum are inclusive 
    },
    randomComic: function() {
      this.number = this.getRandom(1, this.max);
    },

This will choose a random integer between 1 and the maximum number supported by XKCD, and set number to this new number. Given our previous logic, the new comic will be fetched automatically.

To make this work, we need to keep track of the max. Add that to the data:

  data: {
    number: '',
    max: '',
    current: {},
    loading: true,
  },

Then modify the watcher:

  watch: {
    number: function(value,oldvalue) {
      if (oldvalue === '') {
	this.max = value;
      } else {
	this.xkcd();
      }
    },
  },

The max is simply the number fetched the first time we load the page.

If you tested the previous code we wrote, you'll notice the Next button doesn't work well when we are viewing the latest comic. This is because an error occurs when loading the comic (view the console). Now that we have a maximum, we can also use it when an error occurs fetching the comic from the API:

      }).catch(err => {
	this.number = this.max;
      });

We'll just load the comic with the largest available number.

Adding comments

The last feature we'll ad is the ability for people to add comments. We won't require people to login yet, because we haven't covered authentication yet and don't have a server to provide authentication of users. So we'll just build a commenting front end.

In index.html, add the following right after the comic, still inside the div with the v-else:

	<h3>Add a Comment</h3>
	<form v-on:submit.prevent="addComment">
	  <input v-model="addedName" placeholder="Name"></p>
	  <textarea v-model="addedComment"></textarea>
	  <br/>
	  <button type="submit">Comment</button>
	</form>
	<h3>Comments</h3>
	<div v-for="comment in comments[number]">
	  <hr>
	  <p>{{comment.text}}</p>
	  <p><i>-- {{comment.author}}</i></p>
	</div>

This adds a comment form, plus code to display all comments left for this comic. Notice that comments are going to use an object, with an array of comments for each comic number.

In script.js, add some new variables of data:

    addedName: '',
    addedComment: '',
    comments: {},

The addedName and addedComment variables are used in the comment form. The comments variable is used to store all the comments.

Then add the addComment method:

addComment: function() {
      if (!(this.number in this.comments))
	Vue.set(app.comments, this.number, new Array);
      this.comments[this.number].push({author:this.addedName,text:this.addedComment});
      this.addedName = '';
      this.addedComment = '';
    },

This method needs to add a new array to the comments object whenever the first comment is made for a given comic. We can't add it the usual way because Vue has to be able to track changes to comments. Instead, we have to use Vue.set to add the array.

Once this is done, we can use standard JavaScript to push the new comment into the array.

You should now have comments working, with separate comments for each comment.