CSCI 338: Fall 2023

Software Engineering

CSCI 338: Fall 2023

Assignments > Lab 6: Intro to Client-Side Programming with "Vanilla JavaScript"

Due on Thu, 10/19 @ 11:59PM. 6 Points.

To become familiar with client-side software engineering considerations, we need a concrete example to work with. Given this, we have decided to delve into client-side web programming – mostly because web clients are accessible and ubiquitous (and client-side web programming is a valuable skillset).

Therefore, in this lab, you will learn a bit about HTML, CSS, and JavaScript. Please refer to the web resources below to familiarize yourself with these languages. We will also be doing a very brief “crash course” of these languages during class.

1. Background Readings and Resources

2. Set-up

Note: Lab 6 builds on Lab 5

In order to begin Lab 6, your Lab 5 code needs to be working correctly. If you haven’t yet finished Lab 5, please make it a priority.

After completing Lab 5, you will create a new branch from your existing lab05-your-username branch (from within your class-exercises-fall2023 repository) as follows:

1
2
3
4
git status  # make sure you've committed all of your files
git branch  # verify that you're on the lab05-your-username branch
git checkout -b lab06-your-username  # should create a new branch based on your lab05 branch
git branch  # verify that you're on your new branch

By branching from your lab05-your-username branch, your Lab 5 code will be included in your lab06-your-username branch (so that your client can interact with it). When you’re done, please make the following modifications to your code:

1. Edit server.py

At the top of server.py, add the following import statement:

1
from fastapi.staticfiles import StaticFiles

Then, at the very bottom of server.py, add this line:

1
app.mount("/", StaticFiles(directory="ui", html=True), name="ui")

This code allows us to server static files from the ui directory as if they were coming from the root of the website. Verify that you did it correctly by trying to access these static files via FastAPI (note that the Docker container you made in Lab 5 must be running):

If you see a JavaScript file and a CSS file, it worked.

2. Create a new HTML file

Within the ui directory, create a new file called index.html and add the following code to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/css/tasks.css">
    <script src="/js/main.js" type="text/javascript" defer></script>
    <title>Web Client</title>
</head>

<body>
    <header>
        <h1>My Tasks</h1>
    </header>

    <main>

        <section>
            <h2>Task List</h2>
            <div class="task-list">
                <!-- tasks go here -->
            </div>
        </section>

        <form class="add-task">
            <h2>Add New Task</h2>
            
            <label for="name">Name:</label>
            <input type="text" placeholder="Task name" id="name">
            
            <label for="description">Description:</label>
            <input type="text" placeholder="Task description" id="description">
            
            <button>Add</button>
        </form>

    </main>
</body>

</html>

3. Add a new stylesheet

Within the ui/css directory, create a new file called tasks.css and add the following code to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
body {
    font-family: Arial, Helvetica, sans-serif;
    margin: 0;
}

header {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 75px;
    border-bottom: solid 1px #000;
    margin-bottom: 20px;
}

main {
    max-width: 600px;
    margin: auto;
}

section,
form {
    border: solid 1px #000;
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 20px;
}

.item {
    display: grid;
    grid-template-columns: auto 100px;
    align-items: center;
    margin-bottom: 10px;
    border: solid 1px #000;
    padding: 10px;
}

.item strong,
.item p {
    grid-column: 1 / 2;
    margin: 0;
}

.item p {
    grid-row: 2 / 3;
}

.item button {
    grid-row: 1 / 3;
}

.add-task {
    display: grid;
    grid-template-columns: 100px auto;
    row-gap: 10px;
    column-gap: 10px;
    align-items: center;
}

.add-task h2 {
    grid-column: 1 / 3;
}

.add-task button {
    grid-column: 2 / 3;
    justify-self: flex-start
}

label {
    text-align: right;
    font-weight: bold;
    font-size: 0.9em;
}

input {
    border: solid 1px #CCC;
    border-radius: 4px;
    padding: 6px 12px;
}

button {
    padding: 6px 12px;
    border-radius: 4px;
    border: solid 1px #FFF;
}

form button {
    background-color: rgb(79, 67, 182);
    color: white;
}

button:hover {
    border: solid 1px #CCC;
    font-weight: 600;

}

You don’t really need to understand the style declarations in any detail (save that for CSCI 344). What you do need to know is that this file is in charge of styling your web client and controlling the layout.

4. View your starter client

When you’re done with steps 1-3 above, you should be able to view your starter client as follows (make sure your Docker container from Lab 5 is running):

If you see something like this in your web browser, you’re all set up:

Screenshot of what your starter client should look like

3. Your Tasks

Now that you’ve set up your “starter client,” your job is to get your client to interact with the server routes that you implemented in Lab 5 using the browser’s built-in fetch API.

When you are finished with all of the tasks described in this section, your client should function like this:

Animation of what the final product should look like

Since this is not a webdev class, we’re just going to ask you to interact with three of the routes you made in Lab 5:

Route Method Description
/tasks GET Reads (downloads) the task list from the server
/tasks POST Creates (adds) a new task on the server in the following format:
{ "name": "Task 1", "description": "Some description." }
/tasks/<id> DELETE Deletes the task stored in the id slot of the array. Example:
/tasks/3 will delete the task stored in array position 3.

1. Read (download) and display all of the tasks

To display all of the tasks on the client, you are going to create some JavaScript functions. Before you begin, take a look at index.html. Note that at the top of the file, there’s a link to main.js, which indicates that the webpage will have access to all of the logic in the main.js file.

1
    <script src="/js/main.js" type="text/javascript" defer></script>

The “defer” attribute indicates that the script will only run after the entire HTML page has been loaded and rendered. Currently, main.js outputs “Hello world” to the console. Use the browser’s built-in inspector to view the console output.

Get the tasks

Your first job is to create a JavaScript function to fetch all of the server tasks, and then display them to the screen. To do this, add the following function definition and invocation to main.js:

1
2
3
4
5
6
7
8
9
// definition
async function getTasks() {
    const response = await fetch("/tasks");
    const tasks = await response.json();
    console.log(tasks);
}

// invocation
getTasks();

Now, refresh your web browser and take a look at the console. It should have outputted all of your tasks to the screen. A few notes on this code:

Display a task

Now that you know how to get data back from the server using fetch, your next step is to display the tasks in a visual form on the browser screen. To do this, we need to build some HTML. Let’s create a function that converts a task object to an HTML snippet:

1
2
3
4
5
6
7
8
function taskToHTML(task) {
    return `
        <div class="item">
            <strong>${task.name}</strong>
            <p>${task.description}</p>
        </div>
    `;
}

This function takes a task objet as an argument and returns an HTML representation of the task. A few notes about this code:

Display all of the tasks

Finally, you’re going to modify the getTasks() function you just made so that it iterates through the task array and appends each HTML representation of the task to the screen.

Final code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function taskToHTML(task) {
    return `
        <div class="item">
            <strong>${task.name}</strong>
            <p>${task.description}</p>
        </div>
    `;
}

async function getTasks() {
    // access an existing HTML element (from index.html):
    const listEl = document.querySelector(".task-list");
    listEl.innerHTML = "";

    // go get the data:
    const response = await fetch("/tasks");
    const tasks = await response.json();
    console.log(tasks);

    // append each task as an HTML element to the DOM:
    if (tasks.length === 0) {
        listEl.innerHTML = "No tasks found.";
    } else {
        // loop through 
        tasks.forEach((task, idx) => {
            listEl.insertAdjacentHTML("beforeend", taskToHTML(task));
        });
    }
}

// kick off the fetch
getTasks();

This code should display all of the tasks to the screen.

Pro Tips

  • Before moving on to the next section, make sure you can follow the logic of the code above (as you’ll need to understand it for Homework 2).
  • To test the code, add some hard-coded tasks to taskdb in the server.py file (see below).
1
2
3
4
5
6
7
taskdb = [{
    "name": "Lab 5",
    "description": "Finish implementing and writing tests for Lab 5"
}, {
    "name": "Topic 8 Readings",
    "description": "Make sure you do this week's readings for CSCI338!"
}]

2. Create new tasks on the server from the web client

We can now fetch and display tasks (from the server). But how do we create new tasks? Well, we can use fetch for this too!

Creating a hard-coded task

To create a new task, we’ll need to issue a POST request to /tasks. To do this, we’ll start by adding another function to main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// function definition:
async function createTask() {

    // create a fake task:
    const name = "New Task";
    const description = "Description of new task.";
    const newTask = {
        "name": name,
        "description": description
    }

    // post the task to the server using the "POST" method:
    const response = await fetch("/tasks", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(newTask),
    });
    data = await response.json();
    console.log(data);

    // requery and redraw the tasks (lazy, but OK for now)
    await getTasks();
}

// function invocation:
createTask();

After adding this code, refresh your browser. You should see that a new task has been created. Refresh your browser again. You should see another new task (with the same name and description).

Please study the code above carefully:

Creating a task when the user clicks the add button

Hopefully you’re asking yourself: how do I only create a new task when I actually click the “Add” button (versus when the page loads)? Well, to do this, we need to add an event handler to our form, and override the default action that the browser takes when a form gets submitted.

HTML Edit

Please add an event handler to the form tag of index.html. The event handler tells your browser that when the user submits the form, it should invoke the createTask function:

1
<form class="add-task" onsubmit="createTask(event)">
JavaScript Edit

You will also need to modify the createTask function as follows:

1
2
3
4
5
6
7
8
// function definition:
async function createTask(ev) {
    ev.preventDefault();

    // everything else stays the same
    ...
}
// createTask();

This change allows us to pass a browser event object as an argument into the function (so that we can override its behavior). The ev.preventDefault() invocation basically says to the browser, “instead of doing what the form is supposed to do, execute my code instead.” Note that you will also comment out the createTask() invocation so that the function not longer runs on pageload.

Now that you have made these changes, you can add a new task by clicking the “Add” button.

Allowing the user to specify the name and description

Although we’re making progress, we’re still not able to name or describe the task (because it’s hard coded). To fix this, let’s have our function read from the form inputs given by our user by making the following changes to the createTask() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// function definition:
async function createTask(ev) {
    ev.preventDefault();

    // modify the name and description variables to read from the DOM (instead of using hard-coded values):
    const name = document.getElementById("name").value;
    const description = document.getElementById("description").value;

    // everything else stays the same
    ...



    // at the very end, clear out the form:
    document.getElementById("name").value = "";
    document.getElementById("description").value = "";

}

Now test again by filling in actual values into the form fields. Hopefully, you can now add your own tasks to the server!

3. Delete a task

You’ve now created a bunch of tasks! But how do you delete them? To answer this question, we need to both add a new deleteTask() function and also modify the taskToHTML() function so that each task has a corresponding delete button. Please make the following modifications:

A. taskToHTML() changes

Modify the taskToHTML() function as follows:

1
2
3
4
5
6
7
8
9
function taskToHTML(task, idx) {
    return `
        <div class="item">
            <strong>${task.name}</strong>
            <p>${task.description}</p>
            <button onclick="deleteTask(${idx})">Delete</button>
        </div>
    `;
}

B. deleteTask()

To complete this tutorial, you will add a deleteTask() function:

1
2
3
4
5
6
7
8
async function deleteTask(idx) {
    const response = await fetch(`/tasks/${idx}`, { method: "DELETE" });
    const data = await response.json();
    console.log(data);

    // requery and redraw the tasks (lazy, but OK for now)
    await getTasks();
}

Study this function. You should be able to figure out what it does (ask if you don’t understand something…or google it…or use ChatGPT).

Create, read, and delete have now been implemented. If you want, you can implement update (PUT) for extra credit.

4. What to Turn In

Please create a pull request with the fully implemented web client (which should be completed inside of your version of your lab05 folder).