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.
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:
id="search"
to the input
element<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.
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.
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)
..innerHTML
or .textContent
to do that.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'})
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.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:
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:
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.
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:
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
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:
In both cases, the data comes wrapped within the parens of myFunction
.