We have our little app looking just the way we want, but it doesn’t actually do anything yet. So, let’s work on adding some JavaScript to make the app interactive.
This part of the series will require some concentration from you, but even if you’re not a seasoned JavaScript developer, I promise you will walk away having learned some valuable skills.
Initial Setup
First, we’ll create our javascript file assets/js/main.js
and inside of that we will create an object called Gimmie
(named after our app). This is where we will store the variables and functions we need so that they’re not on the window
object. We’ll put it right next to jQuery’s “document ready” call that we’ll also be using.
var Gimmie = { $content: $('.content'), $form: $('form'), }; $(document).ready(function(){ // On page load, execute this... });
Notice that we added a few variables in our Gimmie
object: $content
, $form
. These are jQuery objects, so we name them with a $
in front to remind us of that. Since they are specific DOM nodes that we will reference more than once we will store them in variables for faster, future use.
Form Submission
The first thing we need to handle is when the user enters something into our form and submits it. So, inside our “document ready” we will attach a listener event to our form. We could do $('form').on()
but because we’ve already stored the form element in a variable, we’ll just reference that instead, by doing Gimmie.$form.on()
. Then we will prevent the default form action (so the page doesn’t refresh):
$(document).ready(function(){ Gimmie.$form.on('submit', function(e){ e.preventDefault(); // Do more stuff here... }); });
Loading
Now, we want to show a “loading” state when a user submits the form. That way they know something is happening. If you remember, we designed this in Sketch:
To accomplish this, we will create a function inside our Gimmie
object called toggleLoading
and we can execute it when the user submits a form by calling Gimmie.toggleLoading()
inside our form submission listener. We named it toggleLoading
because we will toggle the current loading state in the UI, i.e. we execute it once on submit, then we’ll run a bunch of code, and when we’re done we will run it again to take away the loading state.
var Gimmie = { /* our other code here */ toggleLoading: function(){ // Toggle loading indicator this.$content.toggleClass('content--loading'); // Toggle the submit button so we don't get double submissions // http://stackoverflow.com/questions/4702000/toggle-input-disabled-attribute-using-jquery this.$form.find('button').prop('disabled', function(i, v) { return !v; }); }, } $(document).ready(function(){ Gimmie.$form.on('submit', function(e){ e.preventDefault(); Gimmie.toggleLoading(); // call the loading function }); });
Notice that we are toggling a class on the .content
element called content--loading
. We need to create some CSS styles around that. So, in our CSS file, let’s add:
.content--loading:before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #fff; opacity: .9; z-index: 10; } .content--loading:after { content: url('../img/loading.gif'); position: absolute; left: 50%; top: 3em; margin-left: -16px; margin-top: -16px; z-index: 11; }
Here we use pseudo elements to create a loading state on our content area. Our before
element is used to create a slightly opaque white overlay on the content area. Then we use the after
element to display an animated loading gif inside our box. When this class is added to our content section, it will appear as if something is loading.
At this point, if you enter anything into the input field and submit the form, a loading state will display and you’ll just be stuck at that.
Form Validation
Before we submit a request to the iTunes API, let’s make sure that the data being entered by the user is correct.
So what exactly constitutes correct data? Well, if we look at the iTunes API documentation there are a few different ways of getting content. One way is to do a search based on key terms. But what we’re going to do instead is called a “lookup”. From the docs:
You can also create a lookup request to search for content in the stores based on iTunes ID
It then gives a few examples, such as looking up the Yelp Software application by its iTunes ID: https://itunes.apple.com/lookup?id=284910350
. Notice that the application’s ID number is what is shared between these URLs. That’s what we will need from the user.
From a UX perspective, asking for an identifier to an app in the app store might prove to be a little difficult (especially for novices). So instead of instructing people how to get an app’s ID, we’ll ask for the app’s store link. Everyone knows how to copy and paste links! App links are readily available to anyone by simply copying them from the individual app’s page in the app store (in iTunes, on the web, or in the Mac App store).
So, once the user enters a link, we need to validate it:
- Make sure it is a valid url that begins with
http://itunes
- Make sure it contains an ID
To do this, we’ll create a validate function inside of our Gimmie
variable and execute it on our form submit event listener.
var Gimmie = { /* our prior code here */ userInput: '', userInputIsValid: false, appId: '', validate: function(input) { // validation happens here }, } Gimmie.$form.on('submit', function(e){ /* our previous code here */ Gimmie.userInput = $(this).find('input').val(); Gimmie.validate(); if( Gimmie.userInputIsValid ) { /* make API request */ } else { /* throw an error */ } });
Note what we’re doing in the code above:
- We add a few variables and one function to
Gimmie
-
userInput
is a string and is set as the input from the user -
userInputIsValid
is a boolean which will be true or false depending on whether the input from the user is valid (we’ll write those tests in a moment) -
appId
is a string of digits which will be extracted fromuserInput
if it’s valid -
validate
is a function where we will validate the user’s input when called
-
- On form submit, we:
- Set
Gimmie.userInput
to the value of the form’s input field - Execute the validation function in
Gimmie.validate()
- Run an if/else statement. If the user input is valid (something our
Gimmie.validate
will determine) then we will proceed and make an iTunes API request. If it’s not valid, we will display an error informing the user that the data they entered is not correct.
- Set
Now let’s write the code that validates whether or not the user input is correct. Note that in our HTML we set our input type to url <input type="url">
. This means some browsers will natively perform some type of validation on that input, however it will not be consistent or uniform across browsers. In some browsers, it won’t even work. So if the user types “blah”, the browser will accept it and the form will submit. In other browsers, they will at least have to type something that begins with “http://” before the browser will let them submit the form. But what we want is a URL that begins with “http://itunes”, so we will handle that in JavaScript.
var Gimmie = { /* our prior code */ validate: function() { // Use regex to test if input is valid. It's valid if: // 1. It begins with 'http://itunes' // 2. It has '/id' followed by digits in the string somewhere var regUrl = /^(http|https):\/\/itunes/; var regId = /\/id(\d+)/i; if ( regUrl.test(this.userInput) && regId.test(this.userInput) ) { this.userInputIsValid = true; var id = regId.exec(this.userInput); this.appId = id[1]; } else { this.userInputIsValid = false; this.appId = ''; } } }
Here we are using regular expressions to test whether or not the input meets our conditions. Let’s walk through this in a little more detail:
var regUrl = /^(http|https):\/\/itunes/i; var regId = /\/id(\d+)/i;
These are the two regular expression literals we define (read more about regular expressions). Here’s a brief summary of what those regular expressions are doing:
-
regUrl
is the regex literal for determining whether or not the user input is a URL that begins with “http://itunes”-
/^
says “start the regex and begin looking at the beginning of the string” -
(http|https):
says “look for either ‘http’ or ‘https’ followed by a semicolon ‘:’” -
\/\/
says “look for ‘//’” (because a forward slash is a special character in regular expressions, like the way we use it at the beginning of the regex, we have to use the backslash before the forward slash to tell the regex not to interpret the forward slash as a special character, but rather as just a forward slash) -
itunes/i
says “look for ‘itunes’ then end the regex” while thei
at the end indicates the matching should be done case insensitive (since somebody might paste ‘HTTP://ITUNES’ that is still valid)
-
-
regId
is the regex literal for determining whether or not the user input has an ID of digits matching the iTunes store link URL pattern. All valid iTunes store links will have/id
in it, followed by a sequence of numbers.-
/\/id
says “start the regex and look anywhere in the string for ‘/id’” (see previous example for forward slash escaping description) -
(\d+)
says “look for a sequence of digits (0 through 9) 1 or more times and capture them”. The parenthesis indicate that we want to remember whatever match is defined inside, i.e. in our case we remember the sequence of digits which represents the app’s ID. The/d
is a special character in regex indicating we want digits [0-9] and the+
says match those [0-9] 1 or more times. -
/i
says “end the regex” and thei
indicates case-insensitive matching (since a URL with/ID938491
is still valid)
-
The next bit of our code looks like this:
if ( regUrl.test(this.userInput) && regId.test(this.userInput) ) { this.userInputIsValid = true; var id = regId.exec(this.userInput); this.appId = id[1]; } else { this.userInputIsValid = false; this.appId = ''; }
The .test()
method on a regex literal looks for a match from a specified string and returns true or false, depending on if the specified match is found. So, in our case, we test the user’s input against both regUrl
and regID
to ensure that the input both begins with “http://itunes” and has “/id” followed by a number of digits.
If both tests return true, we set the userInputIsValid
flag to true and then we extract the ID from the URL and set it as appId
. To do this, we run the .exec()
method on the input. This will return an array of two items: one matching the entire regex, and one matching just the string of numbers after /id
(this is because we used the parenthesis in the regex (/d+)
to say “remember what gets capture here”). So, as an example, the .exec()
method would return something like ["/id12345", "12345"]
and we want just the second item in the array, so we set appId
to that.
If both tests return false, we set userInputIsValid
to false and the appId
to an empty string.
Now everything we need to determine whether the user’s input is valid is in place. So we can continue with the rest of our script.
Throwing Errors
Right now we are at a point now in our script where we can determine whether or not the user’s input is what we need. So we left off with this:
if( Gimmie.userInputIsValid ) { /* make API request */ } else { /* throw an error */ }
For now, we are going to handle the “throwing an error” part. Because there are going to be other spots in the execution of our script where errors can occur, we are going to make a generic error function that will display an error to the user, depending on what went wrong. If you remember, we designed what this would look like in Sketch:
Notice how our error state has essentially two items: a “header” that is bolded text, and a “body” which is regular set text. So we will create a generic error function which accepts these. The “header” will generally state the error and the “body” will describe how fix the error. So, in this particular case, if the input from the user is invalid we need to inform them what a proper input type is. So, let’s create a generic function that can show errors based on the text we pass it:
if( Gimmie.userInputIsValid ) { /* make API request */ } else { Gimmie.throwError( 'Invalid Link', 'You must submit a standard iTunes store link with an ID, i.e. <br> <a href="https://itunes.apple.com/us/app/twitter/id333903271?mt=8">https://itunes.apple.com/us/app/twitter/<em>id333903271</em>?mt=8</a>' ); }
Here, we call the function Gimmie.throwError()
and pass it two parameters: the “header” text and the “body” text. Because we are just displaying HTML, we can pass HTML elements inside the parameters if we wish. In this case, in the “body” we pass in an example iTunes store link and we highlight the id/
part with the emphasis tag (<em>
) to help indicate to the user, “hey, we need an iTunes store link, and make sure it has an ID in it”.
We can set our CSS to highlight whatever is wrapped in <em>
tags, as well as giving our error state a color:
.content--error { color: #196E76; } .content em { font-style: normal; background-color: lightyellow; }
Now, we will create the throwError
function in the Gimmie
object:
var Gimmie = { /* prior code here */ throwError: function(header, text){ this.$content .html('<p><strong>' + header + '</strong> ' + text + '</p>') .addClass('content--error'); this.toggleLoading(); } }
Notice that we are grabbing this.$content
. This is the same as doing $('.content')
but we saved that selection as a variable in the Gimmie
object, since we will use it more than once. So we reference it by doing this.$content
. Now we set the HTML contents of the $content
element to the text we passed in, with the “header” text being bolded. Then we add a class of content--error
to our content element that way we can style the errors how we wish. Lastly, we run the toggleLoading()
function of Gimmie
to remove the loading class and stop displaying the loading gif.
At this point, if you type in an incorrect URL like http://google.com
, or if you type in a proper iTunes URl that doesn’t have an ID like https://itunes.apple.com/us/app/twitter/
you should see an error message displayed:
To enhance our form a bit, let’s add a nice little “pop” animation that runs when an error occurs (in supported browsers). To do this, we’ll add/remove a CSS class that contains an animation. So, in our CSS file, let’s do:
.content--error-pop { -webkit-animation: pop .333s; -moz-animation: pop .333s; -o-animation: pop .333s; animation: pop .333s; } @-webkit-keyframes pop { 0% {-webkit-transform: scale(1);} 50% {-webkit-transform: scale(1.075);} 100% {-webkit-transform: scale(1);} } @-moz-keyframes pop { 0% {-webkit-transform: scale(1);} 50% {-webkit-transform: scale(1.075);} 100% {-webkit-transform: scale(1);} } @-o-keyframes pop { 0% {-webkit-transform: scale(1);} 50% {-webkit-transform: scale(1.075);} 100% {-webkit-transform: scale(1);} } @keyframes pop { 0% {-webkit-transform: scale(1);} 50% {-webkit-transform: scale(1.075);} 100% {-webkit-transform: scale(1);} }
That will make the content area scale up and down in size, making it “pop” when an error occurs. Now we just have to add/remove that class with JavaScript. So back in our throwError
function:
throwError: function(header, text){ // Remove animation class this.$content.removeClass('content--error-pop'); // Trigger reflow // https://css-tricks.com/restart-css-animation/ this.$content[0].offsetWidth = this.$content[0].offsetWidth; // Add classes and content this.$content .html('<p><strong>' + header + '</strong> ' + text + '</p>') .addClass('content--error content--error-pop'); this.toggleLoading(); },
Here we remove the class first, then we trigger a “reflow” to ensure that the animation starts again when we add the class in the next step (along with the regular content--error
class). Now we have a nice pop animation on our error state as well:
Making an API Request
We are getting close to finishing now. We’ve checked to make sure the input from the user is correct and we’ve provided a way to display errors, so now we just need to make our API request.
We’ll do this inside our if()
statement that validates the user’s input.
if( Gimmie.userInputIsValid ) { $.ajax({ url: "https://itunes.apple.com/lookup?id=" + Gimmie.appId, dataType: 'JSONP' }) .done(function(response) { // when finished }) .fail(function(data) { // when request fails }); } else { /* our other code here */ }
As you can see from the code above, we’ve setup an AJAX request to the iTunes API. As you may remember from earlier, the iTunes API has a “lookup” URL we can hit to get data back. It follows this format: https://itunes.apple.com/lookup?id=
followed by the ID of the thing you want to find. The API gives an example of a software application lookup using the Yelp app: https://itunes.apple.com/lookup?id=284910350. If you go to that URL in your browser, you’ll see a mess of JSON:
If you run that through a linter, like JSON lint, the results will be formatted and will begin to make a lot more sense:
If you look at the API documentation, you’ll notice that the API provides results for all kinds of content in the iTunes store, everything from music to movies to apps. That is advantageous to us because it means we can grab icon artwork not just for iOS apps, but Mac apps as well! Mac apps use the same type of URL structure as iOS apps. For example, Final Cut Pro has a link of https://itunes.apple.com/us/app/final-cut-pro/id424389933?mt=12. Note that the URL begins with https://itunes
and has /id424389933
, which is just what we need!
Using our error function from earlier, let’s throw an error for if/when our API request fails:
if( Gimmie.userInputIsValid ) { $.ajax({ url: "https://itunes.apple.com/lookup?id=" + Gimmie.appId, dataType: 'JSONP' }) .done(function(response) { // when finished }) .fail(function(data) { Gimmie.throwError( 'iTunes API Error', 'There was an error retrieving the info. Check the iTunes URL or try again later.' ); }); } else { /* our other code here */ }
As we abstracted our method for displaying errors into a single function, displaying another error is easy!
The Response
Now let’s worry about what happens when the request completes successfully:
$.ajax({ /* other code here */ }) .done(function(response) { // Get the first response and log it var response = response.results[0]; console.log(response); }) .fail(function(data) { /* other code here */ });
Note here that we are getting the response and logging the first result to the console. If you look at an example API request you’ll see that at the top most level of the JSON object, you get resultCount
which tells you how many results there are (in a lookup there should only be one) and then results
which is an array (with a single object in this case) which represents the result.
So, we set the response to the first item in results and then log it to the console. If you open our little app in the browser and enter a URL (for example, the Yelp URL https://itunes.apple.com/lookup?id=284910350
) you’ll see the UI be stuck in the loading state, but if you look at the development tools and go to the console, you’ll see our API response logged. We can now access any of those properties in JavaScript!
As you can see, the API returns a bunch of information about the app: its name, developer, description, genre, price, and much more! We really only need a few of those things, like the app’s icon. So, we will check to just make sure our request contains the pieces of information we need.
$.ajax({ /* other code here */ }) .done(function(response) { // Get the first response and log it var response = response.results[0]; console.log(response); // Check to see if request is valid & contains the info we want // If it does, render it. Otherwise throw an error if(response && response.artworkUrl512 != null){ Gimmie.render(response); } else { Gimmie.throwError( 'Invalid Response', 'The request you made appears to not have an associated icon. <br> Try a different URL.' ); } }) .fail(function(data) { /* other code here */ });
Here we check to make sure response
exists and we check to make sure response.artworkUrl512
is part of that response. artworkUrl512
is the key that the API provides for a link to the full size app icon. If those things are present, we are going to display the app icon on the page. For that, we have another function called render
which we will write in a moment. If for some reason the stuff we need is missing, we throw another error with our nice function we already made.
Rendering the API Results
Now that we have the API returning the data we want, let’s render the results on the page. Once we know we have everything we need from the API, we call Gimmie.render(response)
and pass the API response to it, which is just an object of key/value pairs. So, back in our Gimmie
object, let’s create the render
function:
var Gimmie = { /* our other code here */ render: function(response){ var icon = new Image(); icon.src = response.artworkUrl512; icon.onload = function() { Gimmie.$content .html(this) .append('<p><strong>' + response.trackName + '</strong></p>') .removeClass('content--error'); Gimmie.toggleLoading(); } } }
Here’s what we are doing in this code:
- To create an image from scratch, we create a variable named
icon
and use theImage()
constructor which basically creates an HTMLImageElement. Think of it like creating an<img>
tag in memory using JavaScript. - We then set the
src
attribute of our image by using theicon.src
method available to use because we used theImage()
constructor. We set the image’s source to theartworkUrl512
of our response. This will make the browser begin fetching the image at that specified URL. - We use
icon.onload
to tell the browser “when you’re done fetching the image, do this…”. This is a way of having the browser fetch an image resource and then not actually put it into the DOM until it’s been downloaded. - Inside of
icon.onload
we set the HTML of$contents
to the image we just retrieved. Then we can append more information to that content area if we want. In this example, I grab thetrackName
from our API response to show the app’s name along with its icon. - Lastly execute our
toggleLoading
function because we’re done loading everything!
Try running this in your browser now and you should see a nice icon show up! For example, try the Yelp URL https://itunes.apple.com/us/app/yelp/id284910350?mt=8
Try it with a Mac app URL, like Final Cut Pro https://itunes.apple.com/us/app/final-cut-pro/id424389933?mt=12
Loading the Icon Mask
Notice that the iOS icon is not rounded. As noted earlier, most iOS icons are not designed with rounded corners. Those are applied at the OS level. For iOS icons, we will have to apply the mask we created in Sketch. So, if you go into Sketch and export that mask we created as an image asset, we’ll load that into the browser when we load the icon:
var Gimmie = { render: function(response){ var icon = new Image(); icon.src = response.artworkUrl512; icon.onload = function() { Gimmie.$content .html(this) .append('<p><strong>' + response.trackName + '</strong> Actual icon dimensions: ' + this.naturalWidth + '×' + this.naturalHeight + '</p>') .removeClass('content--error'); Gimmie.toggleLoading(); // If it's an iOS icon, load the mask too if(response.kind != 'mac-software') { var mask = new Image(); mask.src = 'assets/img/icon-mask.png'; mask.onload = function() { Gimmie.$content.prepend(this); } } } } }
Here’s what we are doing:
- In our API results, there’s an object called “kind” which refers to the kind of thing being returned by the API, like movie or music or software. Mac apps will have a “kind” of “mac-software”. Because Mac apps don’t need a mask applied to their icon, we check to see if the response kind is not “mac-software”. If it’s not, we know it’s an iOS app and then we can load the mask
- We use the same
Image()
constructor as before, set thesrc
of the image to where we saved our mask, and then prepend it to our content area once theonload
event fires for the image.
Now we just have to add some styles for positioning the mask over the icon:
.content img[src*="icon-mask.png"] { position: absolute; left: 0; top: 0; }
That’s it! If you enter the Yelp URL again, this time it will appear with rounded corners!
Complete!
It’s been quite a journey, and I hope you’ve learned a lot from this tutorial! We covered creating wireframes and mocks for our app and its different states. We also covered writing HTML, CSS, and Javascript for a web app that interfaces with a third-party API.
Hopefully you’ve grasped a few basics of interfacing with an API. You’ve learned how to dynamically pull content and assets from an API and render it all into a web page. With that basic knowledge you can now move onto creating more personalized web apps tailored to your interests.
Here’s a list of a few APIs out there:
Comments