- Write a front-end client that interacts with the YouAreEll RESTful API.
- The client should visually display the user's message threads, with each thread containing a list of messages to and from another user.
- The client should allow the user to post a new message to a particular user.
- No front end frameworks or libraries, including JQuery.
- This project uses the latest JavaScript features, many of which are not available in browsers without using a transpiling technology. To avoid using a transpiller, you MUST USE GOOGLE CHROME for this lab.
- To establish familiarity with
- HTML
- HTML forms
- CSS
- JavaScript
- JavaScript Modules
- The Document Object Model
- Http requests
- Your project contains two files,
index.htmlandstyles.css, providing you with the basic html structure of the project and some basic styles.
- Navigate to your project directory in the command line. Run the command
python -m SimpleHTTPServer 8000. This will expose the project onlocalhost:8000. Navigate there in your browser to view your project.
- Create a new file in the project directory called
index.js. - Link this file in the
<head>of yourindex.htmlfile, using the<script>tag.- In addition to src, you'll need two extra attributes on your
<script>tag,typeandasync. - For the
typeattribute, assign it a value ofmodule. This denotes that the file should be treated as a JavaScript module. Normally, JavaScript files are executed immediately once they are downloaded, even if the HTML hasn't finished parsing yet. We'll explore the benefits of JavaScirpt modules throughout this lab, but one benefit is that the executive ofmodulesisdeferreduntil after the HTML is Parsed. Read more about JavaScript modules - For the
asyncattribute, assign it a value oftrue. Typically, when an HTML file hits a<script>tag, it stops parsing the HTML, downloads the JavaScript, and then executes the JavaScript.async="true"overrides this behavior, instead allowing the HTML to be parsed alongside downloading the JavaScript. Once the JavaScript is finished downloading, the HTML parsing is paused and the script executes. Read more about async and defer
- In addition to src, you'll need two extra attributes on your
- At the top of your
index.htmlfile, declare a new variable calledcurrentUserand assign it your YouAreEll username (You should have made one in the previous YouAreEll lab). - Add an event listener to the
windowobject. TheaddEventListenermethod takes two parameters, the type of event you're listening for (examples include "load", "click", "keydown", etc), and a function reference, known as a callback, representing the function you want to invoke when the event occurs. Wraping code in a "load" event listener attached to thewindowobject will insure that your code is only ran once the page has loaded.
let userId = "dominiqueclarke";
window.addEventListener("load", function () {
});- Our goal is to add some text to the
<h2element, nested within theheaderelement containing theidofgreeting. In order to do so, we need to grab this element off thedocumentobject - Use the
getElementByIdmethod to grab the element containing the idgreeting. This will return to you an object of typeelement, allowing you to use any of the methods or access any of the properites available on the element interface. - Assign the
innerHTMLproperty the template string`Welcome ${userId}`
let userId = "dominiqueclarke";
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
});- Refresh your page to view your changes
- Create a new JavaScript file called
message-serivce.js. This file will contain a JavaScript class calledMessageService, responsible for making HTTP requests to fetch and update data from the YouAreEll RESTful API.
class MessageService {
}- Configure your
MessageServiceas a module.- In JavaScript, the word "modules" refers to small units of independent, reusable code. They are the foundation of many JavaScript design patterns and are critically necessary when building any non-trivial JavaScript-based application. The closest analog in the Java language are Java Classes. However, JavaScript modules export a value, rather than define a type. In practice, most JavaScript modules export an object literal, a function, or a constructor. Modules that export a string containing an HTML template or a CSS stylesheet are also common.
- The
exportstatement is used when creating JavaScript modules to export functions, objects, classes or primitive values from the module so they can be used by other programs with the import statement. exportyourMessageServiceas thedefault.
export default class MessageService {
}- Import your MessageService module into your
index.jsfile using theimportstatement. This creates a global variable containing the exported value from the imported module.
import MessageService from "./message-service.js";
let userId = "dominiqueclarke";
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
});- Create a new
MessageServiceobject by using thenewkeyword to invoke theMessageServiceconstructor.
import MessageService from "./message-service.js";
let userId = "dominiqueclarke";
const messageService = new MessageService();
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
});- In
message-service.js, create a method calledgetAllMessages, which takes 0 parameters - Create a
XMLHTTPRequest(XHR) object and assign it to a variable calledrequest. XMLHttpRequest (XHR) objects interact with servers throughHTTPrequests. You can retrieve data from a URL without having to do a full page refresh. XMLHttpRequest is used heavily in Ajax programming. - Use the
openmethod on therequestobject, passing the type ofHTTPrequest you'd like to make and the request endpoint as the first two arguments. To get all the global messages, use the/messages/endpoint. Refer back to the original YouAreEll lab for documentation on the API if necessary. - Use the
sendmethod to send the request. This method takes an optional parameter of the requestbodywhen necessary.
export default class MessageService {
getAllMessages() {
let request = new XMLHttpRequest();
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
}
}- We've configured and sent the request, but what happens when we receive the request back? We can define a function to be used once the response is received using the
onloadproperty of therequestobject.
getAllMessages() {
let request = new XMLHttpRequest();
// Setup our listener to process compeleted requests
request.onload = function() {
// do something
};
request.open("GET", `http://zipcode.rocks:8085/messages`);
request.send();
}- If the status is greater than or equal to 200 and less than 300, than we have a successful response. Else, we have an error. Create an if/else statement to handle the response or error.
- The response is stored in the
responseTextproperty of therequestobject as an array of JSON objects. To convert it into an array of JavaScript objects, useJSON.parse(request.responseText).
getAllMessages() {
let request = new XMLHttpRequest();
// Setup our listener to process compeleted requests
request.onload = function() {
if (request.status >= 200 && request.status < 300) {
console.log(JSON.parse(request.responseText)); // 'This is the returned text.'
} else {
console.log('Error: ' + request.status); // An error occurred during the request.
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
}- Test the function by navigating back to
index.jsand invoking the function.
import MessageService from "./message-service.js";
let userId = "dominiqueclarke";
const messageService = new MessageService(userId);
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
messageService.getAllMessages();
});- Refresh your browser. Right click on the page and select
inspect. When the dev tools container pops up, click theconsoletab. Once the response is returned, you should see the returned array of messages printed to the console.
- Our current
getAllMessagesmethod has some issues. XMLHTTPRequests are processed asynchronously using callbacks. Callbacks cannot contain a return value. This makes it difficult to pass back a value toindex.jswhere thismessageService.getAllMessages()is being called. Fortunately, we can alieviate this issue usingpromises.- A Promise is an object representing a contract to preform some task asynchronous (often, an
HTTPrequest), providing a value (often, anHTTPresponse) when the task is complete. - Promises allow us to continue running syncronous code while waiting for for the execution of the promised task.
- Promises allow us to specify a function that should be run once the task is complete using the
thenmethod. - Promises are tricky. Familiarize yourself with Promises with this tutorial
- A Promise is an object representing a contract to preform some task asynchronous (often, an
- Wrap your
request.onloadfunction in anewPromise;
getAllMessages() {
const request = new XMLHttpRequest();
new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
console.log(JSON.parse(request.responseText)); // 'This is the returned text.'
} else {
console.log('Error: ' + request.status); // An error occurred during the request.
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
});
}- If the request is successful,
resolvethepromisepassing in thethreadsobject``
getAllMessages() {
const request = new XMLHttpRequest();
new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
const threads = JSON.parse(request.responseText); // 'This is the returned text.'
resolve(threads);
} else {
console.log('Error: ' + request.status); // An error occurred during the request.
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
});
}- If the request returns an error,
rejectthepromisepassing in thethreadsobject``
getAllMessages() {
const request = new XMLHttpRequest();
new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
const threads = JSON.parse(request.responseText); // 'This is the returned text.'
resolve(threads);
} else {
reject({
status: request.status,
statusText: request.statusText
});
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
});
}- Specify the function you'd like executed when the promise is resolved by using the
thenmethod.- The
thenmethod is part of thePromiseinterface. It takes up to two parameters: acallbackfunction for the success case and a callback function for the failure case of thePromise. - If the
Promiseis successful, the first parameter (the success callback), is executed. If thePromiseresults in an error, the second parameter (the failure callback), is excuted.
- The
getAllMessages() {
const request = new XMLHttpRequest();
new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
// If successful
const threads = JSON.parse(request.responseText);
resolve(threads);
} else {
reject({
status: request.status,
statusText: request.statusText
});
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
}).then(successCallback, errorCallback);
function successCallback() {
console.log("Promise is successful!");
}
function errorCallback() {
console.log("An error occurred");
}
}- When the callbacks are executed, the receive a special parameter. The success callback receives the value passed to the
resolvemethod, while the failure callback receives the value passed to therejectmethod.
getAllMessages() {
const request = new XMLHttpRequest();
new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
// If successful
const threads = JSON.parse(request.responseText);
// this data is passed to the success callback
resolve(threads);
} else {
// this data is passed to the failure callback
reject({
status: request.status,
statusText: request.statusText
});
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
}).then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
console.log(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}
}- By refactoring our
getAllMessagesmethod, we can consume thePromisewithin ourindex.jsfile, allowing for separation of concerns. - Remove the
thenmethod,successCallbackdeclaration anderrorCallbackdeclaration fromgetAllMessages. returnthe Promise from thegetAllMessagesmethod. This will allow us to call thethenmethod, passing in the appropriate success and failure callbacks, elsewhere.
getAllMessages() {
const request = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
// If successful
const threads = JSON.parse(request.responseText);
// this data is passed to the success callback
resolve(threads);
} else {
// this data is passed to the failure callback
reject({
status: request.status,
statusText: request.statusText
});
}
};
request.open("GET", "http://zipcode.rocks:8085/messages");
request.send();
})
}- Navigate back to your
index.jsfile.getAllMessagesnow returns aPromise. We can now use thethenmethod to specify acallbackfunction to be executed in case of success or failure of thatPromise. Call.thenonmessageService.getAllMessages, reimplementing the original code.
messageService.getAllMessages()
.then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
console.log(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}- Now that we have our messages, let's add them to our page visually. Using the DOM interface, we can create and add HTML elements to our page.
- We'll populate our messages inside the unordered list
<ul id="message-list">.
- We'll populate our messages inside the unordered list
- Create a new function in
index.jscalledpopulateMessages.populateMessagesshould take one parameter, a list of messages.
import MessageService from "./message-service.js";
let userId = "dominiqueclarke";
const messageService = new MessageService(userId);
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
messageService.getAllMessages()
.then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
console.log(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}
});
function populateMessages(messages) {
}- In order to add content to the
DOM, we need to create newnodes. Anodeis an interface is an interface from which a number ofDOMAPI object types inherit, includingdocument,elementand more. Anoderepresents a piece of theDOMtree. - Using a
forEachloop, loop through each message in the array ofmessages. - For each message, create a new
<li>elementto hold the sender username and the message content and assign it tomessageListItem.- You can do this by calling the
createElementmethod on thedocumentobject, passing in the element tag name as a string. This will return a new HTMLelementthat you can later append to theDOM. Remember,elementsare a type ofnode.
- You can do this by calling the
- For each message, create a new
<h3>element for the sender username and assign it toconst userIdHeading. - For each message, create a new
<p>element for the message content and assign it toconst messageParagraph.
function populateThread(messages) {
messages.forEach(message => {
const messageListItem = document.createElement("LI");
const userIdHeading = document.createElement("h3");
const messageParagraph = document.createElement("p");
})
}- Both our
<h3>element and our<p>element will contain text.- To add new text to our page, we need to first create a new
text node. You can create atext nodeusing thecreateTextNodemethod on thedocumentobject, passing in the text you wish to include in the node. This will return a newtext nodethat you can later append to anelement.
- To add new text to our page, we need to first create a new
- For each message, create a
text nodeusing thefromidproperty on themessageobject and assign it to constuserIdContent. - For each message, create a
text nodeusing themessageproperty on themessageobject and assign it toconst messageContent.
function populateThread(messages) {
messages.forEach(message => {
const messageListItem = document.createElement("LI");
const userIdHeading = document.createElement("h3");
const messageParagraph = document.createElement("p");
const messageContent = document.createTextNode(message.message);
const userIdContent = document.createTextNode(message.fromid);
})
}- Now that we've created these text nodes, we need to add them to our new html elements.
- To add any node to another node, use the [
appendChild] method. TheNode.appendChild()method adds a node to the end of the list of children of a specified parent node.appendChildreturns the modifiednodeobject, allowing you to perform method chaining.
- To add any node to another node, use the [
- Add your
messageContentnodeto yourmessageParagraphnodeusing theappendChildmethod. - Add your
userIdContentnodeto youruserIdHeadingnodeusing theappendChildmethod. - Add both your
userIdHeadingnodeand yourmessageParagraphnodeto yourmessageListItemnode, using theappendChildmethod and method chaining.
function populateThread(messages) {
messages.forEach(message => {
const messageListItem = document.createElement("LI");
const userIdHeading = document.createElement("h3");
const messageParagraph = document.createElement("p");
const messageContent = document.createTextNode(message.message);
const userIdContent = document.createTextNode(message.fromid);
userIdHeading.appendChild(userIdContent);
messageParagraph.appendChild(messageContent);
messageListItem
.appendChild(userIdHeading)
.appendChild(messageParagraph);
})
}- By using these methods, we've created a complete
DOMnodefor each message that includes an<li>containing a<h3>elementfor themessage.fromIdand an<p>elementfor themessage.message. - Now that we've created our new
node, we need to add it to an existing HTMLelementon our page. Review theindex.htmlfile and find<ul id="message-list">. We want to add all of our new individual<li>elements to this<ul>. To grab thiselementusing javascript, we can use thegetElementByIdmethod on thedocumentobject, passing in the element'sidas a string. - Using the
appendChildmethod, append themessageListItemnodeto theelementreturned usingdocument.getElementById("message-list"). This will add a new<li>representing each message to our<ul id="message-list">element.
function populateThread(messages) {
messages.forEach(message => {
const messageListItem = document.createElement("LI");
const userIdHeading = document.createElement("h3");
const messageParagraph = document.createElement("p");
const messageContent = document.createTextNode(message.message);
const userIdContent = document.createTextNode(message.fromid);
userIdHeading.appendChild(userIdContent);
messageParagraph.appendChild(messageContent);
messageListItem
.appendChild(userIdHeading)
.appendChild(messageParagraph);
document.getElementById("message-list").appendChild(messageListItem);
})
}- Now that we've created our message, let's invoke the function from our
successCallbackmethod, passing in the array ofmessagesreturned from our HTTP request.
window.addEventListener("load", function () {
document.getElementById("greeting").innerHTML = `Welcome ${userId}!`;
messageService.getAllMessages()
.then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
populateMessages(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}
});- Refresh your page to review the results and check for any errors
- Now that we've fetched all the current messages, let's send new messages out into the atmosphere.
- Navigate to your
message-service.jsfile. Add a new method calledcreateNewMessage. It should take one parameter, the newmessageobject. - Set up your
XMLHTTPRequest. The set up is the same as ourgetAllMessagesmethod, except for calling therequest.openandrequest.sendmethods. - To add a new message to the database, we need to use the HTTP
POSTmethod. In therequest.openmethod, pass in"POST"as the first parameter, and the Post endpoint as the second parameter. The endpoint to send a new message is/ids/:mygithubid/messages/. Refer back to the original YouAreEll lab for documentation on the API if necessary. - For
HTTPmethods where a requestbodyis necessary, pass the request body as a parameter to therequest.sendmethod. To send ourmessageobject as therequestbody, first convert it from a JavaScript object to a JSON object using theJSON.stringifymethod.
createNewMessage(message) {
const request = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
// Setup our listener to process compeleted requests
request.onload = function () {
// Process the response
if (request.status >= 200 && request.status < 300) {
// If successful
resolve(JSON.parse(request.responseText));
} else {
reject({
status: request.status,
statusText: request.statusText
});
}
};
request.open("POST", `http://zipcode.rocks:8085/ids/${message.fromid}/messages`);
request.send(JSON.stringify(message));
});
}- Navigate to your
index.jsfile. Notice that in ourindex.htmlfile, we have aform. This form exists to create and send new messages. In order to set up the form to listen to input from the user and respond propertly to the user hitting the submit button, we need to set up aneventListenerfor our form. - Create a new function in
index.jscalledcreateFormListener. This method takes 0 parameters.
function createFormListener() {
}- Grab the
formelementusingdocument.getElementByIdpassing in theidof theform. - Set the onsubmit property of the
formto a function reference. This function takes one parameter,event. This function will fire when the form is submitted. - To prevent the default form action from occuring, use the
preventDefaultmethod on theeventobject.
function createFormListener() {
const form = document.getElementById("new-message-form");
form.onsubmit = function (event) {
// stop the regular form submission
event.preventDefault();
}
};- Navigate to
index.htmland find theformelement. Notice that theformcontains two form elements,textareaandbutton.textareahas anattributeofnameset to the propertymessage. When form elements are given anameattribute, it adds information about that element theformobject as a property. - Create a object called
datawith two properties,fromidandmessage.fromidshould be assigned the value ofuserid, and message should be assigned the value ofform.message.value(the value of the textarea with attributename="message"). - Call the
createNewMessagemethod on themessageServiceobject, passing in thedataobject. ThecreateNewMessagemethod returns aPromise, so specify your success and failurecallbacksusing thethenmethod. - In your
successCallbackmethod, invoke the populateMessages
function createFormListener() {
const form = document.getElementById("new-message-form");
form.onsubmit = function (event) {
// stop the regular form submission
event.preventDefault();
const data = {
fromid: userId,
message: form.message.value
};
messageService.createNewMessage(data)
.then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
console.log(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}
}
};- Just like we added our array of messages from before, we now need to add our new message to our list of messages.
- Navigate to your
index.jsfile. Add a method calledaddMessageToThread. The method should take on parameter, a singlemessage. - Like before, we need to create a bunch of individual nodes and combine them together in order to create a full
<li>element containing a message.
function addMessageToThread(message) {
const messageListItem = document.createElement("LI");
const userIdHeading = document.createElement("h3");
const messageParagraph = document.createElement("p");
const messageContent = document.createTextNode(message.message);
const userIdContent = document.createTextNode(message.fromid);
userIdHeading.appendChild(userIdContent);
messageParagraph.appendChild(messageContent);
messageListItem
.appendChild(userIdHeading)
.appendChild(messageParagraph);
document.getElementById("message-list").appendChild(messageListItem);
}- Does this code look familiar? Before we move forward, let's go back and refactor our
populateThreadmethod to use thisaddMessageToThreadmethod.
function populateMessages(messages) {
messages.forEach(message => {
addMessageToThread(message);
})
}- Navigate back to your
createFormListenermethod. In thesuccessCallback, invoke theaddMessageToThreadmethod, passing in the response, instead of logging the response.
function createFormListener() {
const form = document.getElementById("new-message-form");
form.onsubmit = function (event) {
// stop the regular form submission
event.preventDefault();
const data = {
fromid: userId,
message: form.message.value
};
messageService.createNewMessage(data)
.then(successCallback, errorCallback);
function successCallback(response) {
// This data comes from the resolve method
addMessageToThread(response);
}
function errorCallback(response) {
// This data comes from the reject method
console.log(response);
}
}
};- Navigate back to your browser and refresh. Type a message into the form and hit submit. Scroll down to the bottom of the list to see your new message.
- Bonus:
- Try to make the new message append to the top, instead of the bottom OR
- Try to make the the message container stay scrolled to the bottom