Coding Lab 1 - namwkim/cs179-coding-lab GitHub Wiki
-
Download VSCode
-
Download the base code
-
Open the code folder using VSCode
-
Install the Live Server plugin
- To load files and images through the browser (Cross-Origin Resource Sharing).
- For Atom, install the atom-live-server package
- Or, if Python installed, run
python -m SimpleHTTPServer 8000on the code folder. - Or, if you use
npm, I recommend the `http-server' package.
-
Click "Go Live" from the VSCode status bar:

-
You will see the following page in your browser:

The base code currently displays a static web page. The goal of this lab is to make it (slightly) interactive so that you can like (double-click an image) or comment on a post: Try it here
The base code has the following structure:
/img
/css
- main.css
- nav.css
- post.css
/js
- main.js
- utils.js
index.html
In this lab, we are only interested in index.html and main.js.
In the HTML file, main.js file is included as a module (type="module") as below:
<script src="js/main.js" type="module"></script>It means you can use the new import & export syntax in ES6 in the main.js file. This file basically serves as an entry point to other javascript files.
In addition, main.js is run after all HTML elements are loaded. As a result, you no longer have to use window.onload to wait until the document is fully loaded.
The index.html file also includes CSS files. For modularity, CSS files are separated into three different files. nav.css contains a stylesheet for the navigation bar at the top, post.css contains a stylesheet relevant to the post in the middle of the page, and main.css file contains all other styles. While not discussed in this activity, CSS Flexbox is extensively used to create the page layout.
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/post.css">The only part relevant to this activity is the following code, which is currently empty but will be dynamically filled by using Javascript. We will get to this part soon.
<main class="main">
<div class="posts">
<!-- posts will be dynamically added using javascript -->
</div>
</main>This javascript file contains all the code you need for this activity. The current code generates a static web page; that is, no interactions are supported. You need to fill in missing codes to make the web page interactive. To guide you, I left comments where you need to code. Please look for TODO # in the file.
Let's briefly go over the code to get you started .
See the top line in the file. You can easily import functions from another file utils.js, thanks to ES6. Without it, you need to include each script in the HTML file. If your app depends on many scripts, you know what happens. For more info about the import syntax, see here.
// import some utility functions
import { uniqueId, timespan } from './utils.js';Next, you see the data that is used to create posts.
// this is the underlying data of the web page
let posts = [
{
id: "post_jd4111h4", // post id
userId: "nkim", // user id who created this post
userImg: "/img/nkim.png", // thumbnail image of the user
img: "/img/post1.jpg", // post image
likes: 0, // # of likes
datetime: "2018-02-02T00:40:33.000Z", // time posted
comments: [ // user comments for this post
{
userId: "nkim", // user id who commented
text: "Let's Go Travel!" // text of the comment
}
]
},
{
id: "post_jd5ikpep",
userId: "nkim",
userImg: "/img/nkim.png",
img: "/img/post2.jpg",
likes: 0,
datetime: "2018-02-01T00:40:33.000Z",
comments: []
}
];It is an array containing a list of posts, while each post in the array is an object which has a dictionary structure with keys and properties. You also see an array of comments in each post.
To familiarize yourself with JSON (JavaScript Object Notation), let's try adding a new post to the data.
Hints:
- You can use any image (e.g.,
img/posts3.jpg). - To generate a unique id, use the
uniqueIdimported fromutils.js. - For example,
uniqueId('post_')will generate a unique id with the prefix 'post_'. - To fill in the
datatimeproperty, you can usenew Date().toISOString()which formats the current date toYYYY-MM-DDTHH:mm:ss.sssZ. - You can add a new post by directly editing the data above or using a syntax like
posts.push({...}).
Similarly, add a new comment for the new post.
Print the data using console.log. You can see the printed data in the Web Console. To open the Web Console using Chrome, 1) right-click on a web page, 2) click 'inspect' in the context menu, and 3) click 'Console' tab at the top tool bar as below:

In order to dynamically render posts and comments as HTMLs, here are two functions that generate HTML texts for posts and comments using a template string (ES6).
function renderComment(comment) {
return `<div class="comment">
<a class="id" href="/">${comment.userId}</a><span>${comment.text}</span>
</div>`
}
function renderPost(post) {
return `<div class="post">
<header class="header">
<a class="photo" href="/">
<img src="${post.userImg}">
</a>
<a class="id" href="/">${post.userId}</a>
</header>
<div class="content">
<img class="image" src="${post.img}"/>
</div>
<div class="footer">
<div class="action">
<a class="icon ${post.likes > 0 ? 'heart' : 'like'}" href="/">Like</a>
<a class="icon comment" href="/">Comment</a>
<a class="icon save" href="/">Save</a>
</div>
<div class="likes">
${post.likes} likes
</div>
<div class="comments">
${post.comments.map(c => renderComment(c)).join('')}
</div>
<time class="time">
${timespan(post.datetime).toUpperCase()} AGO
</time>
<div class="add-comment">
<input type="text" placeholder="Add a comment…"></input>
</div>
</div>
</div>`
}Take a careful look the code, especially the renderPost function, and think about how a post and its comments will be rendered on the web page.
Explain in your words how comments will be rendered based on this code: comments.map(c=>renderComment(c)).join('').
Also, briefly explain what happens when post.likes is greater than zero
- Hint: multiple classes
Lastly, what is generated from the renderPost is a pure string, not a DOM element. How can we handle user interactions? We can add event listeners only to the DOM instance, not the string. Thus, we will add event listeners later on not in this function.
That said, when user actions happen on a specific post, we should be able to know which post it is. Fortunately, data-attribute in HTML allows us to embed custom data into HTML and retrieve it later using Javascript.
Let's add a data-attribute called data-post-id in the root div (i.e., class="post") and set the value as the id of each post.
Here is our root render function. This will retrieve the posts element in the HTML file using querySelector, and set its innerHTML as the string generated by renderPost and renderComment. The innerHTML completely replace the contents of the element using the given string. Unlike querySelectorAll, querySelector returns only the first element matching the selector.
function render(posts) {
// get the posts element
let postsElm = document.querySelector('.posts');
// redner posts inside postsElm
postsElm.innerHTML = posts.map(p => renderPost(p)).join('');
let imageElms = document.querySelectorAll('.post .content .image');
imageElms.forEach(el => el.addEventListener('dblclick', function () {
let postId = this.parentNode.parentNode.getAttribute('data-post-id');
increaseLike(posts, postId);
}));
let commentElms = document.querySelectorAll('.post .add-comment input');
commentElms.forEach(el => el.addEventListener('change', function () {
let postId = null;
addComment(posts, postId, 'kgajos', this.value);
this.value = ''; //empty
}))
}As previously mentioned, we did not yet set event listeners. Once innerHTML is set, DOM elements for posts and comments are instantiated. The rest of the render function finds necessary elements (images and inputs) and add event listeners to them.
Explain what is selected by the following code
let imageElms = document.querySelectorAll('.post .content .image');In the code above, we are adding two interactions. First, adding a listener for double-click on the post image and another listener for a change on the comment section. It is looping over all images of the posts and adding the listener to each image. Same for the comment input.
When the double-click happens, the callback function traverses the DOM tree to retrieve the data-post-id. That is because we need to know which post to update. To traverse up the DOM tree, you can use node.parentNode, to traverse down the DOM tree, you can use node.childNodes.
Print 'this' keyword to the Web Console and explain what's contained in 'this' in the callback. You can test this by double-clicking the image on the web browser.
Once the post's id is found, the increaseLike is called, which is defined as below:
function increaseLike(posts, postId) {
let post = posts.find(p => p.id == postId);// return the first element matching the condition
if (!post) return;
post.likes = post.likes + 1;
localStorage.setItem('posts', JSON.stringify(posts));
render(posts); // re-render with the updated posts
}Look at the last two lines.
Whenever updating the data, we are saving it to localStorage. It allows us to save the data "locally" within the user's browser. It has no expiration date unlike sessions and also dead simple to use. You save data using setItem which accepts two parameters: key and value. The value has to be a string which is why we are using JSON.stringify to serialize the posts.
The last line calls the render function again to re-render the web page with the updated data.
Let's do the same for the comment input. In the change event callback, do the following.
Print 'this.value' to the Web Console. You can test this by writing a comment and pressing 'enter' in the comment section.
And, similar to the image dblclick callback, retrieve the corresponding post's id and pass it to addComment function.
addComment is currently empty. Fill in the function similar to increaseLike.
All done? 👍 💯 🥇 🎉
Here is the last code snippet:
function initialize() {
let initState = localStorage.getItem('posts');
if (initState != null) {
// JSON:
posts = JSON.parse(initState);
}
render(posts);
}
initialize();// call initIt all started from this. It tries to load initial data from localStorage. If no pre-saved data available in localStorage, it uses the data we defined at the top. To test this, interact with the web page and reload the page to see all your interactions are saved and loaded correctly.
If you have time left, try adding more interactions or features to the web page. For example, deleting comments or adding new posts interactively.
