What we'll learn

Requirement :

|- week2-thursday/
  |-- index.html
  |-- style.css
  |-- script.js

Set up

Demo

Box by box, we build

We're gonna build a beautiful dynamic web app today. workshop

We'll do so by using our knowledge of D.O.M. methods.

Now work on these requirements one at a time moving down one box for every feature. Refer to lecture note and google to find the hint

workshop

Requirement :

Create another box at the bottom that contain our Magic-8-Ball game

Instruction:

Previously we have built version 1.0 of Magic-8-Ball It's quite boring to just see "Yes" or "No" all the time. Let's add more possible results!

const yes = ["Yes!", "Sure!", "Of course!"];
const no = ["No way!", "Never!", "Not a chance!"];
function getRandomItem(arr) {
  //code here
}

Instead of changing the HTML text to "Yes" or "No", we change it to a random answer from the appropriate array. For example:

message.innerHTML = getRandomItem(yes);

Note: You can have just one array that holds both "yes" and "no" answers. However, by separating them into 2 arrays, we can have an equal chance of getting either result even if you have more "yes" answers than "no" answers (or the other way around).

Mini Trello

In this section of lab, you will build a Kanban board. A Kanban board is one of the tools that can be used to implement Kanban to manage work personally or at an organizational level. Kanban boards visually depict work at various stages of a process using cards to represent work items and columns representing each stage of the process. A viral Kanban-styled app is Trello.

Implementation

|-mini-trello/
    |- index.html
    |- style.css
    |- script.js

In index.html:

<script type="text/javascript" src="script.js"></script>
<link
  rel="stylesheet"
  href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
  integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
  crossorigin="anonymous"
/>
<link
  href="https://fonts.googleapis.com/css?family=Lato|Roboto"
  rel="stylesheet"
/>

And that's all we need for this mini-project.

The navigation bar contains a burger icon, a search input box, and the title "Mini Trello":

Navbar

<div class="burger">
  <div class="line"></div>
  <div class="line"></div>
  <div class="line"></div>
</div>
<input id="search" placeholder="Search..." autocomplete="off" />
<h1 id="app-title">Mini Trello</h1>

In the main id="app", we'll create 2 section tags. The first one has id="board" and contains logo, descriptiton, and the form to add tasks. The second section in main contains the columns "Backlog", "In Progress", "Review", and "Done".

main

<div id="infos">
  <div class="logo"><img src="logo-12.png" alt="CS Logo" /></div>
  <div id="infos-content">
    <div class="infos-title">FTW Bootcamp</div>
    <div class="infos-description">From Cheetah with love</div>
  </div>
</div>
<div id="add-task-form">
  <input id="task-value" placeholder="Add task" />
  <button id="add-task">+</button>
</div>
<div class="column dropzone">
  <h3 class="column-title">Backlog</h3>
  <ul class="tasks" id="tasks-added">
    <li class="task fill" draggable="true">
      <div class="task-content">Write the weekly note</div>
      <div class="trash">&times;</div>
    </li>
  </ul>
</div>
<div class="column dropzone">
  <h3 class="column-title">In Progress</h3>
  <ul class="tasks"></ul>
</div>
<div class="column dropzone">
  <h3 class="column-title">Review</h3>
  <ul class="tasks"></ul>
</div>
<div class="column dropzone">
  <h3 class="column-title">Done</h3>
  <ul class="tasks"></ul>
</div>

We put a mockup/fake task "Write the weekly note" here to design CSS. We'll remove that li tag.

CSS is not the learning goal of this lab. However, I will quickly explain how the layout is structured here:

Put this CSS code below in style.css:

:root {
  box-sizing: border-box;
}

*,
::before,
::after {
  box-sizing: inherit;
}

body {
  margin: 0;
  padding: 0;
  background-color: #f5f7fa;
}

header {
  display: flex;
  background-color: white;
  height: 60px;
  border-bottom: solid 1px lightgray;
}
#navbar {
  display: flex;
  align-items: center;
  flex: auto;
}
#navbar .burger {
  padding: 18px 18px;
  border-right: 0.5px solid #e0e2e5;
}
#navbar .burger .line {
  width: 24px;
  height: 3px;
  background-color: #e74c3c;
  margin: 4px 0;
}
#navbar #search {
  flex-grow: 1;
  margin-left: 32px;
  font-family: "Lato";
  font-size: 16px;
  border: none;
}
#navbar #search:focus {
  outline: 0;
}
#navbar #app-title {
  margin-top: 22px;
  margin-right: 60px;
  font-family: "Lato";
  font-style: normal;
  font-weight: bold;
  font-size: 24px;
  line-height: 29px;
  color: #4786ff;
}

#app {
  margin-left: 100px;
  margin-right: 100px;
}
#board {
  margin-top: 60px;
  display: flex;
  flex: auto;
  flex-wrap: wrap;
}
#infos {
  display: flex;
  flex-grow: 1;
  margin-bottom: 5px;
}
#infos img {
  width: 50px;
  height: 50px;
  box-shadow: 0px 4px 20px rgba(231, 76, 60, 0.2);
  border-radius: 50%;
}
#infos #infos-content {
  margin-left: 24px;
}
#infos .infos-title {
  font-family: "Lato";
  font-weight: 500;
  font-size: 20px;
  line-height: 24px;
  color: #1b2437;
}
#infos .infos-description {
  font-family: "Lato";
  font-style: italic;
  font-size: 16px;
  line-height: 19px;
  color: #aaacaf;
}

#add-task-form {
  display: flex;
}
#add-task-form #task-value {
  padding-left: 16px;
  margin-top: 1px;
  height: 40px;
  width: 240px;
  border-radius: 0px;
  margin-right: 8px;
  font-family: "Lato";
  font-size: 16px;
  line-height: 19px;
  border: none;
}
#add-task-form #task-value:focus {
  outline: 0;
  box-shadow: 0px 4px 20px rgba(52, 152, 219, 0.4);
}

#add-task-form #add-task {
  background: #4786ff;
  border: none;
  border-radius: 4px;
  width: 40px;
  height: 40px;
  font-family: Lato;
  font-weight: 900;
  font-size: 24px;
  line-height: 29px;
  color: #ffffff;
}
#add-task-form #add-task:focus {
  outline: 0;
}
#add-task-form #add-task:hover {
  box-shadow: 0px 4px 20px rgba(52, 152, 219, 0.4);
}
#columns {
  margin-top: 40px;
  display: flex;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.column {
  flex: 0 0 auto;
  width: 350px;
  height: 520px;
  background: white;
  border-radius: 4px;
  margin-left: 20px;
  background-color: white;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.column:first-child {
  margin-left: 0px;
}
.column:hover {
  border: 1px solid #4786ff;
  box-shadow: 0px 4px 20px rgba(52, 152, 219, 0.4);
}
.column-title {
  margin: 16px 0px 0px 16px;
  font-family: "Lato", Arial, Helvetica, sans-serif;
  font-weight: 600;
  font-size: 1em;
}
.tasks {
  padding: 0 24px;
}
.task {
  list-style-type: none;
  margin-top: 10px;
  position: relative;
  background-color: white;
  border: 1px solid lightgrey;
  border-radius: 4px;
  min-height: 80px;
  width: 288px;
}
.task:first-child {
  margin-top: 32px;
}
.task .task-content {
  margin: 16px 16px 0 16px;
  font-family: "Lato";
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: #54585a;
}
.task:hover {
  background: #ffffff;
  border: 1px solid #4786ff;
  box-sizing: border-box;
  box-shadow: 0px 4px 20px rgba(52, 152, 219, 0.4);
  border-radius: 2px;
}
.task .trash {
  position: absolute;
  top: 5px;
  right: 8px;
  font-family: "Lato";
  font-size: 24px;
  color: #54585a;
}
.task .trash:hover {
  color: #e74c3c;
  cursor: pointer;
}
.hold:active:hover {
  border: solid 3px #4786ff;
}
.hovered {
  border-style: dashed;
}
.invisible {
  display: none;
}

Mini Trello

<!-- <li class="task fill" draggable="true">
  <div class="task-content">Write the weekly note</div>
  <div class="trash">&times;</div>
</li> -->

User can add a new task

When user clicks the Add button, we will take three actions:

Add this code in script.js:

document.getElementById("add-task").addEventListener("click", function () {
  var taskInput = document.getElementById("task-value");
  alert("User entered: " + taskInput.value);
  taskInput.value = "";
});
function addTask(taskValue) {
  var task = document.createElement("li");
  task.classList.add("task");
  task.classList.add("fill");
  task.setAttribute("draggable", "true");
  task.addEventListener("dragstart", dragStart);
  task.addEventListener("dragend", dragEnd);

  var taskContent = document.createElement("div");
  taskContent.classList.add("task-content");
  taskContent.innerText = taskValue;

  var trash = document.createElement("div");
  trash.classList.add("trash");
  trash.innerHTML = "&times;";
  trash.addEventListener("click", removeTask);

  task.appendChild(taskContent);
  task.appendChild(trash);

  var tasks = document.getElementById("tasks-added");
  tasks.insertBefore(task, tasks.childNodes[0]);
}

This is basically the code to generate an li element which contains the task value and a remove button "X". The li element's structure is the same as the mockup li element we have removed in the previous step.

There is something new here though:

task.addEventListener("dragstart", dragStart);
task.addEventListener("dragend", dragEnd);
// ...
trash.addEventListener("click", removeTask);

dragStart and dragEnd are two event handler functions that we need to add to the li element to react when user drag and drop the element to another column.

removeTask is another event handler that will remove the task if the ‘click' event of the "X" button happens.

Implement the event handlers:

function removeTask(event) {
  // event represents the remove button
  // Access the <ul> list by moving 2 levels up
  var tasks = event.target.parentNode.parentNode;
  // Access the <li> element which is the direct parent
  var task = event.target.parentNode;
  tasks.removeChild(task);
}

// DRAG & DROP

// A global variable to store the selected task
var task;

function dragStart(event) {
  event.target.classList.add("hold");
  task = event.target;
  setTimeout(function () {
    event.target.classList.add("invisible");
  }, 0);
}

function dragEnd(event) {
  event.target.classList.remove("invisible");
}

Evaluation

Type something in the Add Task input and click the Add button. You should see the task appears in the Backlog column. You can drag & drop it, but nothing happens for now.

You can click on the remove button to delete the task.

User can drag and drop the task to another column

Put this code in script.js:

function dragEnter(event) {
  if (event.target.classList.contains("dropzone")) {
    event.target.classList.add("hovered");
  }
}

function dragOver(event) {
  event.preventDefault(); // https://stackoverflow.com/a/35428657
}

function dragLeave(event) {
  event.target.classList.remove("hovered");
}

function dragDrop(event) {
  event.target.classList.remove("hovered");
  // event represents the column
  // Add the task to the right child. Inspect the element to find the ul is index 3 in childnodes.
  event.target.childNodes[3].append(task);
}

var dropzones = document.getElementsByClassName("dropzone");

for (var index = 0; index < dropzones.length; index++) {
  const dropzone = dropzones[index];
  dropzone.addEventListener("dragenter", dragEnter);
  dropzone.addEventListener("dragover", dragOver);
  dropzone.addEventListener("dragleave", dragLeave);
  dropzone.addEventListener("drop", dragDrop);
}

The variable dropzones contains all elements that have been assigned to the class dropzone. Those elements are actually the columns Backlog, In Progress, Review, and Done.

The for loop adds the event handler functions dragEnter, dragOver, dragLeave, dragDrop to all columns.

The dragenter event is fired when a dragged task enters a column. When the event is fired, we change the column's border-style to dashed by adding the class hovered to the column element.

The dragleave event is fired when a dragged task leaves a column. We will remove the class hovered to bring the border of the column back to normal.

The drop event is fired when a dragged task is dropped on a column. When the event is fired, we add the task element (stored in the variable task) to the column's ul element.

And that's it. Good job!

Now it's your turn. Let's implement your own features to the app. Use this list below as suggestions. Feel free to go beyond it: