The phrase will be retrieved when the button is clicked. Clicking a button fires an event and the browser checks to see if there is any event handler function that will handle the event.

Don't use inline Javascript

You will see on the Web examples like below:

<input type="button" onclick="handleTheEvent();" value="Search">
or
<button onclick="handleTheEvent();>Search</button>

While such code is easy to write, it mixes HTML and Javascript. It is better to keep Javascript code in a separate file. This means that we will need to bind the event handler function to the element's attribute.

We show first the HTML code for the search box and the button, then the Javascript solution.

<input id="search"> <!-- add id to select this input element -->
<button>Search books</button>
var button = document.querySelector("button");
button.onclick = function () {
  var input = document.querySelector("#search");
  console.log("Your search phrase is: ", input.value);
}

To test how this works, either use your working file (if you have followed along with the AM1 Answer notes), or alternatively, grab this latest version of the AM1 sol file, where we have done these additions:

  • added the attribute id="search" to the input element
  • added the line <script src="books.js"></script> toward the end of the HTML file, just before the </body> tag.

Now test it! Load the HTML page on the browswer, open the Console, type a phrase in the input box and then click the button.

Where to put the script tag?

While the script tag can go everywhere in the HTML document, the best practice is to put it right at the bottom of the HTML document, before the ending body tag. This is because we want to execute the Javascript code, only after all the HTML elements in the page have been created. If not, our Javascript code may fail if it refers to elements that are yet to be created.

How to insert new HTML elements on the fly?

Given that each book will have different pieces of information, we'll need to be constantly creating new elements and insert them into the DOM (for the title, an h2 element, for the thumbnail photo an img element, and so on. The following Javascript methods will be useful:

  • document.querySelector - this method will return an element from the DOM. We will use it to find the parent element where we'll add children elements. For example: document.querySelector("#results")
  • document.createElement - this method will create a new element, given the name of the tag. However, the created element is not added to the DOM yet. Example: document.createElement("ul").
  • objectName.setAttribute - this method sets an attribute of the instance to a given value. For example, given that we have created an element of kind img and stored it in the variable imgEl, we can add its src attribute by writing: imgEl.setAttribute("src", "pelican.jpg").
  • parentName.appendChild - this method will append a created HTML element to a parent node. For example, if we have var divRes = document.querySelector("#results");, we can add the imgEl to it by writing divRes.appendChild(imgEl).
  • Dependenging if we want to add HTML or text inside a node, we can use the properties .innerHTML or .textContent to do that.

Challenge

You can write your code by repeating the above statements a few times (for adding every element), or you can create a function to do that. Given the following function signature and examples of invocation, write the body of the function.

/* Function to create an element and add it to the parent node.
Parameters are the parent selector, the tag of element we'll create, the text
to appear in the element and an object of property:value pairs for the element.
*/

function addElement(parent, child, text, attrObj){
  ...
}

// Instances of invocation:
addElement("#results", "div", "", {'class':'book'})
addElement(".book", "h2", "What a wonderful day"); // no attrObj in this case
addElement(".book", "img", "", {'src': 'pelican.jpg', 'alt': 'book name'})

Sorting Books by Date - Optional

As you might have noticed in the screencast, the books in the search page are ordered from the newest to the oldest. This means that before we create the HTML elements, we need to sort the data. Javascript's sort method is different from that of other languages. By default it sorts only in alphabetical fashion, and doesn't distinguish between strings and numbers (treats everything like a string). Here is a short tutorial from W3Schools on the method. We'll need to write a comparing function to do the right comparison.

If we store all the items in a list "books", we can use the property "publishedDate" of each book time, like in this example:

var books = results.items;
books.sort(function(a,b) { b.volumeInfo.publishedDate - a.volumeInfo.publishedDate; })

Notice that we put b before a in the subtraction, to do a descending sorting.

You can test this in the console (make sure that the file "wc.js" is linked in the HTML page). As you can notice, this sorting is destructive, meaning, it changes the object that is being sorted, instead of creating a new array of sorted objects.

Encoding the URL

As we have seen, the URL that we'll send to Google Books API contains the phrase written by a user in the input box. Such a string will often contain characters that are not allowed in a URL and need to be encoded into HEX representation (from the ASCII table. For example, the space character will be converted into %20, a quotation mark into %22, and so on.

We will use the built-in function encodeURI to perform this encoding. Know that this function has a sibling that performs the reverse operation, decodeURI. More about these methods in the W3 Schools reference page. Here is an example of its use:

Using the encodeURI function.

AJAX - Send the Request

If you want to learn more about this topic, Chapter 6: Talking to the Web from the book "Head First HTML5 Programming" has a good discussion.

AJAX stands for Asynchronuous Javascript and XML. It means that we will send requests to a web server and when the server is ready will send a response in XML format. Nowadays, most web servers send their response in JSON, but the name hasn't changed.

In the following we will show some Javascript code to send the request. We will discuss the jQuery method some other time.

       
var baseURL = "https://www.googleapis.com/books/v1/volumes?q=";
var searchTerm = encodeURI("harry potter"); // this should later get the value from input field
var url = baseURL + searchTerm;

function ajaxRequest(url) {
  var request = new XMLHttpRequest(); 
  request.open("GET", url); 
  
  request.onload = function() {
    if (request.status == 200) {
      displayBooks(request.responseText); // displayBooks is your function
    }
  };
  
  request.send(null); 
}

// Temporary function, to see that the request works
function displayBooks(result){
  alert("got results: " + result.length);
  var results = JSON.parse(result); // response comes as plain text, need to convert it to JSON 
  console.log(results);
}

// invoke the function
ajaxRequest(url);

If you copy this code in your Javascript file, reload your HTML page, two things should happen:

  • You'll see an alert pop up with a message
  • In the console you'll see the result as a JSON object
  • Now that you have this working, you need to assemble the pieces together (get the search query from the input box, create the URL, invoke the ajaxRequest within the onclick event handler) to have a functioning app.

    Same Origin - CORS and JSONP

    Our code above worked, because the Google server puts in the Response Header a field like:

    access-control-allow-origin:*

    This gives permission to the browser to display data that originate from a server different from the server where the current webpage originates. The browser follows a so called Same Origin Policy to protect users from malicious code, knwon as Cross-site Scripting. According to Same Origin Policy Recently, browsers have started to implement a new technique for relaxing the constraints of same Origin Policy, which is known as CORS - Cross Origin Resource Sharing (this is a very good overview). As part of this technique, the server uses the header access-control-allow-origin, to indicate that an origin may request a resource.

    Unfortunately, not all servers use this practice yet. We will notice this, when we use our AJAX function (shown above), which will give us an error message, such as the one shown below, when requesting data from the Tumblr API:

    Error in the console when CORS is not being followed by an API server.

    In these cases the solution is JSONP (JSON with padding), which injects a script tag on the fly, everytime we need to send a request.

    Here is some code for implementing JSONP. It creates a new script elements, sets its attibutes, checks whether an old element of the same kind exist. If no element exists, it simply appends the new one to the head, otherwise it replaces the old element with the new one.

        
    function jsonpRequest(requestURL) {
    
    	var newScriptElement = document.createElement("script");
    	newScriptElement.setAttribute("src", requestURL);
    	newScriptElement.setAttribute("id", "jsonp");
    	var oldScriptElement = document.getElementById("jsonp");
    	var head = document.getElementsByTagName("head")[0];
    	if (oldScriptElement == null) {
    		head.appendChild(newScriptElement);
    	}
    	else {
    		head.replaceChild(newScriptElement, oldScriptElement);
    	}
    }

    This function is also described in details in the Chapter 6 mentioned above.

    For this function to do its job properly, we need to add one parameter to the request URL, the so-called callback function, that is, the function that will be invoked once the results are back. Because the results will already come as a JSON object (instead of a string that need to be converted to JSON), we cannot use the displayBooks function from above. Below is what we need to call the function:

    // This function doesn't need to parse the results into JSON
    function displayBooksFromJSONP(result){
      console.log(result);
    }
    
    // we need to add the callback parameter to the URL
    url = url + "&callback=displayBooksFromJSONP";
    jsonpRequest(url); // invoke function
    

    Is JSONP supported?

    Both CORS and JSONP are intended to deal with client-side programming (running Javascript code on the browser). Such problems are not encountered when we use server-side technology. However, if we want to change only some portions of the web page (and not the entire one), we can only use Javascript to do that, thus, there is no way to avoid them.

    You can see if JSONP is supported by an API, by first creating the URL to receive some data, and then adding the portion &callback=myFuntion at the end of the URL. If the data comes back wrapped in the parentheses of myFunction, then the API supports JSONP. Good API documentations are explicit in revealing whether JSONP is supported or not.

    Here is the example for Google Books:

    https://www.googleapis.com/books/v1/volumes?q=wellesley+college&callback=myFunction

    Here is the example for Tumblr API:

    http://api.tumblr.com/v2/blog/inlovewithjavascript.tumblr.com/posts/text?notes_info=true&reblog_info=true&api_key=2XzL11cJvSJi6Nftq8o87c5xbQkzEKvel8npyO1ohwb3aQP1iI&callback=myFunction

    In both cases, the data comes wrapped within the parens of myFunction.