- 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.html
andstyles.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.html
file, using the<script>
tag.- In addition to src, you'll need two extra attributes on your
<script>
tag,type
andasync
. - For the
type
attribute, 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 ofmodules
isdeferred
until after the HTML is Parsed. Read more about JavaScript modules - For the
async
attribute, 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.html
file, declare a new variable calledcurrentUser
and assign it your YouAreEll username (You should have made one in the previous YouAreEll lab). - Add an event listener to the
window
object. TheaddEventListener
method 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 thewindow
object 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
<h2
element, nested within theheader
element containing theid
ofgreeting
. In order to do so, we need to grab this element off thedocument
object - Use the
getElementById
method 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
innerHTML
property 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
MessageService
as 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
export
statement 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. export
yourMessageService
as thedefault
.
export default class MessageService {
}
- Import your MessageService module into your
index.js
file using theimport
statement. 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
MessageService
object by using thenew
keyword to invoke theMessageService
constructor.
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 throughHTTP
requests. You can retrieve data from a URL without having to do a full page refresh. XMLHttpRequest is used heavily in Ajax programming. - Use the
open
method on therequest
object, passing the type ofHTTP
request 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
send
method to send the request. This method takes an optional parameter of the requestbody
when 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
onload
property of therequest
object.
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
responseText
property of therequest
object 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.js
and 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 theconsole
tab. Once the response is returned, you should see the returned array of messages printed to the console.
- Our current
getAllMessages
method 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.js
where 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
HTTP
request), providing a value (often, anHTTP
response) 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
then
method. - 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.onload
function in anew
Promise
;
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,
resolve
thepromise
passing in thethreads
object``
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,
reject
thepromise
passing in thethreads
object``
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
then
method.- The
then
method is part of thePromise
interface. It takes up to two parameters: acallback
function for the success case and a callback function for the failure case of thePromise
. - If the
Promise
is successful, the first parameter (the success callback), is executed. If thePromise
results 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
resolve
method, while the failure callback receives the value passed to thereject
method.
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
getAllMessages
method, we can consume thePromise
within ourindex.js
file, allowing for separation of concerns. - Remove the
then
method,successCallback
declaration anderrorCallback
declaration fromgetAllMessages
. return
the Promise from thegetAllMessages
method. This will allow us to call thethen
method, 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.js
file.getAllMessages
now returns aPromise
. We can now use thethen
method to specify acallback
function to be executed in case of success or failure of thatPromise
. Call.then
onmessageService.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.js
calledpopulateMessages
.populateMessages
should 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
. Anode
is an interface is an interface from which a number ofDOM
API object types inherit, includingdocument
,element
and more. Anode
represents a piece of theDOM
tree. - Using a
forEach
loop, loop through each message in the array ofmessages
. - For each message, create a new
<li>
element
to hold the sender username and the message content and assign it tomessageListItem
.- You can do this by calling the
createElement
method on thedocument
object, passing in the element tag name as a string. This will return a new HTMLelement
that you can later append to theDOM
. Remember,elements
are 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 node
using thecreateTextNode
method on thedocument
object, passing in the text you wish to include in the node. This will return a newtext node
that 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 node
using thefromid
property on themessage
object and assign it to constuserIdContent
. - For each message, create a
text node
using themessage
property on themessage
object 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.appendChild
returns the modifiednode
object, allowing you to perform method chaining.
- To add any node to another node, use the [
- Add your
messageContent
node
to yourmessageParagraph
node
using theappendChild
method. - Add your
userIdContent
node
to youruserIdHeading
node
using theappendChild
method. - Add both your
userIdHeading
node
and yourmessageParagraph
node
to yourmessageListItem
node, using theappendChild
method 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
DOM
node
for each message that includes an<li>
containing a<h3>
element
for themessage.fromId
and an<p>
element
for themessage.message
. - Now that we've created our new
node
, we need to add it to an existing HTMLelement
on our page. Review theindex.html
file and find<ul id="message-list">
. We want to add all of our new individual<li>
elements to this<ul>
. To grab thiselement
using javascript, we can use thegetElementById
method on thedocument
object, passing in the element'sid
as a string. - Using the
appendChild
method, append themessageListItem
node
to theelement
returned 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
successCallback
method, passing in the array ofmessages
returned 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.js
file. Add a new method calledcreateNewMessage
. It should take one parameter, the newmessage
object. - Set up your
XMLHTTPRequest
. The set up is the same as ourgetAllMessages
method, except for calling therequest.open
andrequest.send
methods. - To add a new message to the database, we need to use the HTTP
POST
method. In therequest.open
method, 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
HTTP
methods where a requestbody
is necessary, pass the request body as a parameter to therequest.send
method. To send ourmessage
object as therequest
body, first convert it from a JavaScript object to a JSON object using theJSON.stringify
method.
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.js
file. Notice that in ourindex.html
file, 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 aneventListener
for our form. - Create a new function in
index.js
calledcreateFormListener
. This method takes 0 parameters.
function createFormListener() {
}
- Grab the
form
element
usingdocument.getElementById
passing in theid
of theform
. - Set the onsubmit property of the
form
to 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
preventDefault
method on theevent
object.
function createFormListener() {
const form = document.getElementById("new-message-form");
form.onsubmit = function (event) {
// stop the regular form submission
event.preventDefault();
}
};
- Navigate to
index.html
and find theform
element. Notice that theform
contains two form elements,textarea
andbutton
.textarea
has anattribute
ofname
set to the propertymessage
. When form elements are given aname
attribute
, it adds information about that element theform
object as a property. - Create a object called
data
with two properties,fromid
andmessage
.fromid
should 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
createNewMessage
method on themessageService
object, passing in thedata
object. ThecreateNewMessage
method returns aPromise
, so specify your success and failurecallbacks
using thethen
method. - In your
successCallback
method, 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.js
file. 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
populateThread
method to use thisaddMessageToThread
method.
function populateMessages(messages) {
messages.forEach(message => {
addMessageToThread(message);
})
}
- Navigate back to your
createFormListener
method. In thesuccessCallback
, invoke theaddMessageToThread
method, 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