This assignment builds on the work you have done for Assignment 1.
In this part, you will be implementing the client-side functionalities of the application. Here is a high-level overview of what you will be doing:
- Turn the application into a Single-page Application by dynamically replacing the DOM depending on the URL in the address bar ("Client-side routing")
- Define high-level objects such as
Room
andLobby
to manage the application state. (Object-Oriented programming) - Define application components like
ChatView
andLobbyView
to dynamically update the DOM tree upon changes in the application state, following a Model-View-Control (MVC) pattern used in many mainstream frameworks like React, Angular, and Vue. - Create event handlers to add interactivity in the application and to update the application state.
Throughout the assignment, we prohibit the use of third-party JavaScript frameworks like React, Angular, or Vue, because we want you to see and experience the inner workings of a web application. After you go through the assignments, hopefully you will understand how these third-party frameworks do their "magic".
Same as assignment 1, except this time it will include a JavaScript file that describes your application.
/client/
/assets/
/index.html
/chat.html (will be deleted at the end of the assignment)
/profile.html (will be deleted at the end of the assignment)
/style.css
/app.js (created in this assignment)
/server.js
/package.json
app.js
will have all your JavaScript code. You should include this in your index.html
as a <script>
tag. At the end of this assignment, you will no longer need chat.html
and profile.html
as we will dynamically generate these pages using JavaScript.
Here are some helper functions you may find useful during development. You are not obliged to use them.
// Removes the contents of the given DOM element (equivalent to elem.innerHTML = '' but faster)
function emptyDOM (elem){
while (elem.firstChild) elem.removeChild(elem.firstChild);
}
// Creates a DOM element from the given HTML string
function createDOM (htmlString){
let template = document.createElement('template');
template.innerHTML = htmlString.trim();
return template.content.firstChild;
}
// example usage
var messageBox = createDOM(
`<div>
<span>Alice</span>
<span>Hello World</span>
</div>`
);
-
(1 Points) [JS] Define a function named
main
in the global scope, and then add it as the event handler for theload
event of thewindow
object. Themain
function will be called once when the page is loaded. We impose strict naming for functions and variables, so that the automated tests can locate them. Therefore, for all of the tasks, please follow the variable naming closely. -
(4 Points) [JS] We will first create a client-side "router" to render pages dynamically based on the URL. The idea is to use the hash
#
part of the URL as the client-side path, preventing the browser from making a GET request to fetch a different page. For example, instead of fetching the profile page at/profile
on the server, we will dynamically render the profile page on the client side when the location hash is#/profile
.- A) Inside the
main
function (from Task 1), define a function namedrenderRoute()
. This function should read the URL from the address bar (hint: usewindow.location.hash
) and then conditionally perform the following:- If the first part of the path is an empty string
""
, it should empty the contents of#page-view
, and then populate it with the corresponding content fromindex.html
from Assignment 1 (i.e.,div.content
). From now on, we will refer to this page as the "lobby page". - If the first part of the path is
"chat"
, it should empty the contents of the#page-view
, and then populate it with the corresponding content fromchat.html
from Assignment 1. From now on, we will refer to this page as the "chat page". - If the first part of the path is
"profile"
, it should empty the contents of#page-view
, and then populate it with the corresponding content fromprofile.html
from Assignment 1. From now on, we will refer to this page as the "profile page". - The helper functions provided above will come in handy for this task.
- If the first part of the path is an empty string
- B) Inside the
main
function, attach therenderRoute
function as the event handler for thepopstate
event of thewindow
object. Thepopstate
event is fired when the URL changes (e.g., when the back button is clicked). - C) Inside the
main
function, call therenderRoute
function once, so that the appropriate page is rendered upon page load. - D) Replace all the
href
attributes of all the links in the HTML by adding a#
character at the beginning of the URL. After you have done this, your application should now be a Single-page Application. Verify that this routing mechanism works by clicking around the links and observing the pages render dynamically.
- A) Inside the
-
(4 Points) [JS] In Task 2, you have generated each page programmatically. However, the generated DOM has no relation to the application state, and is recreated every time the URL changes. In this task, we will create objects to represent each page, and each object will hold the relevant data and handle the events within the page. This software design pattern is called "Model-View-Control".
- A) Define a class named
LobbyView
. Inside the constructor, create the DOM for the "lobby page" (which you have generated in Task 2) and then assign it to theelem
property. - B) Define a class named
ChatView
. Inside the constructor, create the DOM for the "chat page" (which you have generated in Task 2) and then assign it to theelem
property. - C) Define a class named
ProfileView
. Inside the constructor, create the DOM for the "profile page" (which you have generated in Task 2) and then assign it to theelem
property. - D) Inside the
main
function, instantiate the view objects using the classes you have just defined in Tasks 3.A, 3.B, and 3.C. Use the variable nameslobbyView
,chatView
, andprofileView
. - E) Update the
renderRoute
function to use the newly instantiated objects instead (hint: you should reuse the DOM attached to theelem
property of each object).
- A) Define a class named
-
(2 Points) [JS] In this task, you will update the class definitions from Task 3 to store references to some of the DOM elements, which you will use later. This is an intermediate step, so you may not observe any noticeable change in application behaviour.
- A) Inside the constructor of
ChatView
, after you have created theelem
property, create the following properties to store references to the descendant elements. (hint: you can usethis.elem.querySelector
)- i.
titleElem
- store reference to theh4
heading for the room name. - ii.
chatElem
- store reference to thediv.message-list
container. You will dynamically add message boxes inside thisdiv
later. - iii.
inputElem
- store reference to thetextarea
element for entering the message to send. - iv.
buttonElem
- store reference to thebutton
element for sending the message.
- i.
- B) Inside the constructor of
LobbyView
, create the following properties.- i.
listElem
- store reference to theul.room-list
element. You will dynamically add list items on this. - ii.
inputElem
- store reference to theinput
element for entering the new room name. - iii.
buttonElem
- store reference to thebutton
element for creating a room.
- i.
- A) Inside the constructor of
-
(7 Points) [JS] In Task 3, you have defined higher level objects to represent each page, but they don't hold any data. In this task you will define some more objects you will use to represent the application state.
- A) Define a class named
Room
. The constructor should accept 4 arguments (in this order):id
,name
,image
,messages
. Forimage
, provide a default image url (e.g.,assets/everyone-icon.png
), and formessages
, provide an empty array as the default value. Inside the constructor, assign the given arguments to the properties with the same name (id
,name
,image
, andmessages
). - B) In the
Room
class, add a method with the following signature:addMessage(username, text)
. If the giventext
is an empty string or a sequence of whitespaces, the method should simply return. Otherwise, the method should create a new object with the keysusername
andtext
, assigning the given arguments to the corresponding field, then push the object into thethis.messages
array. - C) Define a class named
Lobby
, whose constructor takes zero arguments. Inside the constructor, initialize arooms
property, assigning it an associative array of 4Room
objects indexed by theid
property. Use theRoom
class you defined in Task 4.A to create new instances. In the next Task, we will dynamically render the lobby page from this array. - D) In the
Lobby
class, add a method with the signaturegetRoom(roomId)
. The method should search through the rooms and return the room withid
=roomId
if found. - E) In the
Lobby
class, add a method with the signatureaddRoom(id, name, image, messages)
. The method should create a newRoom
object, using the given arguments, and add the object in thethis.rooms
array.
- A) Define a class named
-
(5 Points) [JS] In this task, you will use the objects you defined in Task 4 to provide the "model" for each of the views, so that the views can be rendered dynamically.
- A) Inside the
main
function, before the instantiation oflobbyView
, create aLobby
object and assign it to the variablelobby
. This object will be the "single source of truth" within the application. - B) Modify the constructor of
LobbyView
to accept a single argumentlobby
. Then inside the constructor, assign the received argument to the property namedlobby
. - C) In the
LobbyView
class, add a method namedredrawList
that takes zero argument. This method should empty the contents ofthis.listElem
, then populate the list dynamically from the array ofRoom
objects inside thelobby
object. Call this method inside the constructor to draw the initial list of rooms. - D) Inside the constructor of
LobbyView
class, attach aclick
event handler on thethis.buttonElem
element. The event handler should get the text value from thethis.inputElem
element, and call theaddRoom
method of thethis.lobby
object. After that, clear the text value ofthis.inputElem
.
- A) Inside the
-
(2 Points) [JS] Clicking the "Create Room" button should now trigger the
addRoom
method properly, but you will not see any changes in the view. This is because we have not yet completed the Model-View-Control loop. More specifically, theLobbyView.redrawList
method hasn't been called yet. However, we do not want to call this method directly in theclick
event handler; rather, we wantredrawList
to be called whenever thelobby
object changes, regardless of which event triggered the update. In other words, we wantLobbyView
object to observe changes in thelobby
object and callredrawList
automatically. Here, we will use the "observer pattern" by implementing a simple event listener on theLobby
object.- A) At the end of the
addRoom
method of theLobby
object, check ifthis.onNewRoom
function is defined. If it is defined, call the function, passing the newly createdRoom
object as the argument. - B) Inside the constructor of
LobbyView
, assign a new anonymous function to thethis.lobby
object'sonNewRoom
property. The anonymous function should accept a single argumentroom
. The function should add a new list item to thethis.listElem
element, generating the DOM with the givenroom
data. Alternatively, you could also simply callredrawList
method (this would be less efficient, as it redraws the entire list). - The mechanism you implemented in this task may seem strange at first, but this is essentially an "unsafe" version of the event listener interface, similar to DOM Event API 1.0 (but not 2.0, which supports multiple event listeners). This event-driven pattern is extremely prevalent in JavaScript applications, so you should spend some time following along the control-flow, if it is unclear.
- A) At the end of the
-
(10 Points) [JS] Finally, you will add dynamism and interactivity in the "chat view". The tasks will be similar to Task 6 and 7, but with an extra bit of complexity.
- A) Declare a global variable
profile
, and assign an object with a single keyusername
, assigning an arbitrary name (e.g."Alice"
). This object will represent the current user. - B) At the end of the
addMessage
method of theRoom
object, check ifthis.onNewMessage
function is defined. If it is defined, call the function, passing the newly created message object as the argument. - C) In the
ChatView
class, define a method with the signaturesendMessage()
. The function should read the text value fromthis.inputElem
and call theaddMessage
method of thethis.room
object. You can pass inprofile.username
as the first argument. After that, clear the text value ofthis.inputElem
. - D) Inside the constructor of
ChatView
, make the following modifications:- i. Add a new property named
room
and assignnull
to it. - ii. Add a
click
event handler to thethis.buttonElem
element. The event handler should call thesendMessage
method. - iii. Add a
keyup
event handler to thethis.inputElem
element. The event handler should call thesendMessage
method only if the key is the "enter" key without the "shift" key.
- i. Add a new property named
- E) In the
ChatView
class, define a method with the signaturesetRoom(room)
. We need this method because unlikeLobbyView
wherethis.lobby
object is fixed,this.room
object can be changed. In this method, perform the following operations:- i. Assign the given
room
argument to theroom
property. - ii. Update the
this.titleElem
to display the new room name. - iii. Clear the contents of
this.chatElem
, and dynamically create the message boxes from thethis.room.messages
array. To distinguish between other users' messages and the current user's, you can compare the username againstprofile.username
. - iv. Assign an anonymous function accepting a single argument
message
tothis.room
object'sonNewMessage
property (i.e., attach a new event listener). The function should add a new message box onthis.chatElem
element. To distinguish between other users' messages and the current user's, you can compare the username againstprofile.username
.
- i. Assign the given
- F) Finally, we want to show the specific chat room when the user clicks on it in the lobby. We return to the
renderRoute()
function inmain
to do this.- i. In the branch that reads if the URL has a
"chat"
path, extract theRoom
object with the current room ID (by calling thegetRoom
function of theLobby
object). - ii. Call the
setRoom
function of thechatView
object with the extracted room object. - iii. Make sure you handle exceptions before the call (i.e. the room exists and is not null).
- i. In the branch that reads if the URL has a
- A) Declare a global variable
To test your code, insert the following script tag within the head
tag of your page, BEFORE your app.js
. It is important that the test script is loaded synchronously before your application script for it to work correctly.
<script src="http://99.79.42.146/cpen322/test-a2.js" type="text/javascript"></script>
In addition to adding the script tag above, you will need to inject some tester code to successfully test Tasks 2, 3, 6, and 8. The testing interface is designed this way because there is no way for a third-party script (i.e. the test script) to observe closure variables unless you expose them explicitly.
For example, in Task 2 you are asked to declare a local function renderRoute
inside the main
function. If you do not expose the name renderRoute
from within the main
function, there is simply no way for the tester to observe renderRoute
since it is a closed variable. To make such variables available, you must invoke the cpen322.export
function as shown below.
// assuming we want to expose `renderRoute` and `lobbyView` from inside `main`
// traditional syntax
function main(){
/* other code */
cpen322.export(arguments.callee, {
renderRoute: renderRoute,
lobbyView: lobbyView
});
}
// concise ES6 property shorthand syntax
function main(){
/* other code */
cpen322.export(arguments.callee, { renderRoute, lobbyView });
}
You will see a red button on the top-right corner of your web page. Click it to test your code. Watch out for any alert messages or console output, which tell you any missing components/functionalities. You are responsible for ensuring that all the functionalities above are implemented correctly - the tests are only there to help you. We reserve the right to test your code with other test cases than the above.
The test script uses certain default values during the test.
- Room ID - the test script expects that you have a Room instance with the ID
"room-1"
. - Default room image URL - the test script expects the default image URL to be
"assets/everyone-icon.png"
.
To set a different default value for your application, you can use cpen322.setDefault
as shown below:
cpen322.setDefault("image", YOUR_IMAGE_URL);
cpen322.setDefault("testRoomId", YOUR_ROOM_ID);
There are 8 tasks for this assignment (Total 35 Points):
- Task 1: 1 Points
- Task 2: 4 Points
- Task 3: 4 Points
- Task 4: 2 Points
- Task 5: 7 Point
- Task 6: 5 Point
- Task 7: 2 Point
- Task 8: 10 Point
Copy the commit hash from Github and enter it in Canvas.
For step-by-step instructions, refer to the tutorial.
These deadlines will be strictly enforced by the assignment submission system (Canvas).
- Sunday, Oct 24, 2021 23:59:59 PDT