Building To Do Application Using Rust

Introduction

In this article, we'll walk you through building a To-Do application using Rust. A To-Do app is like a digital checklist where you can write down tasks you need to do. It's a great way to learn Rust because we'll use its special features that make programming efficient and secure.

Prerequisites

Before we dive into building our To-Do application, let's make sure we have everything we need. We will use Rust for the backend logic, Rocket for handling web requests, and HTML/CSS for creating the user interface.

1. Rust Installation

If you haven't installed Rust on your machine, we need to set it up first. Explore the Introduction To Rust article to learn everything about installation.

2. HTML and CSS Basics

For creating the user interface of our To-Do app, having a basic understanding of HTML and CSS will be beneficial.

Setting Up the Project

To create a new Rust project open the terminal and enter the following command.

cargo new your_project_name

This will create your initial Rust project. If you don't know what cargo is, you can explore this article How To Use Cargo Commands In Rust? Now navigate to the project directory open the "Cargo.toml" file and add the following dependencies in this.

[dependencies]
rocket = "0.4.5"
rocket_codegen = "0.4.5"
serde = { version = "1", features = ["derive"] }
lazy_static = "1.4"

[dependencies.rocket_contrib]
version = "0.4.5"
features = ["handlebars_templates", "tera_templates"]

Now run the following command to install all dependencies.

cargo build

Then navigate to the "src" folder and open the "main.rs" file, use all required crates in this file as follows.

#![feature(proc_macro_hygiene, decl_macro)]
#![allow(unused_imports)]
use rocket::State;
use rocket_contrib::json::{Json, JsonValue};
use rocket::routes;
use rocket::get;
use rocket::post;
use rocket::response::Redirect;
use rocket_contrib::json;
use rocket_contrib::templates::tera::{Context};
use rocket_contrib::templates::Template;
use serde::Serialize;
use serde::Deserialize;
use rocket::FromForm;
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;

Define a structure for task data as it has an ID and description.

#[derive(Clone,FromForm, Serialize, Deserialize, Debug)]
struct Task {
    id: u64,
    description: String,
}

#[derive(Clone,FromForm, Serialize, Deserialize, Debug)]
struct TaskData {
    description:String
}

lazy_static! {
    static ref TASKS: Arc<Mutex<Vec<Task>>> = {
        let mut tasks = Vec::new();
        tasks.push(Task {
            id: 1,
            description: "Initial Task".to_string(),
        });

        Arc::new(Mutex::new(tasks))
    };
}

In this code, we have declared a global mutable vector variable using "lazy_static". 

Now create a get endpoint for the homepage.

#[get("/")]
fn index() -> Template {
    // Access the tasks vector from the shared state
    let tasks = TASKS.lock().unwrap();
    println!("{:?}",tasks.clone());
    println!("{}",tasks.len());
    
    Template::render("home", tasks.clone())
}

In this code we are defining an index() function that will return a template, let's create a template page. 

First, create a templates folder in the root directory then navigate to the templates folder and create a new handlebars file named "home.hbs". Update the code of this file as follows.


<head>
    <meta charset="utf-8">
    <title>To Do</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
  <style type="text/css">
 body {
        background: #fff;
        font-family: Arial, Helvetica, sans-serif;
        height: 100vh;
        margin: 0;
    }

    .container {
        margin-top:75px;
        width:300px;
        padding: 20px;
        border-radius: 8px;
        background-color: #fff;
        box-shadow: 0 0 10px rgba(0, 128, 255, 0.8); 
        animation: glow 1s infinite alternate; 
    }

    @keyframes glow {
        to {
            box-shadow: 0 0 20px rgba(0, 128, 255, 0.8); 
        }
    }


    .completed-task{
            text-decoration-line: line-through;
    }
    input {
        border: none;
        background: none;
        flex-grow: 1;
        margin-right: 10px;
        padding: 8px;
    }

    button {
        padding: 10px;
    }
   ul {
    list-style-type: none;
}
  </style>
</head>
<body>
    <div class="container">
        <div>
            
            <div>
                <h3 class="text-center mb-4">My Tasks</h3>
                <ul class="task-list">
                {{#each this}}
                <li class="task-item mb-2">
                    <div class="row">
                        <div class="col-sm-10">
                            <input type="checkbox" class="form-check-input" id="task{{this.id}}" data-id="{{this.id}}">
                            <label class="form-check-label task-description" for="task{{this.id}}">
                                {{this.description}}
                            </label>
                        </div>
                        <div class="col-sm-2 pt-1">
                            <a href="/remove/{{this.id}}">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                            <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
                            <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
                            </svg>
                            </a>
                        </div>
                    </div>
                </li>
                {{/each}}
                <form method="post" action="/create">
                        {{!-- <label for="description">Name your task</label> --}}
                        <input name="description" placeholder="Name your task" required></input>
                        <button class="btn btn-primary btn-lg mt-4" type="submit">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
                            <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
                            <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
                            </svg>
                        </button>
                </form>
            </ul>
            </div>
        </div>
    </div>

    <script>
        // Add event listeners to checkboxes
        const checkboxes = document.querySelectorAll('.form-check-input');
        checkboxes.forEach(checkbox => {
            checkbox.addEventListener('change', function () {
                const taskId = this.getAttribute('data-id');
                const taskLabel = document.querySelector(`.form-check-label[for=task${taskId}]`);
                taskLabel.classList.toggle('completed-task', this.checked);
            });
        });
    </script>
</body>

In this code, we have created the basic UI structure of our todo application. Now return to our "main.rs" file and define the functions for adding new tasks and deleting existing tasks. Open the "main.rs" file and add the following functions to it.

#[post("/create", data="<form_data>")]
fn create(form_data: rocket::request::Form<TaskData>) -> Redirect {
    let mut tasks = TASKS.lock().unwrap();

    println!("{:?}", form_data);
    let new_task = Task {
        id: (tasks.len()+1) as u64,
        description: form_data.description.clone(),
    };
    tasks.push(new_task.clone());
    println!("{:?}", new_task);

    Redirect::to("/")
}

#[get("/remove/<id>")]
fn remove(id: u64) -> Redirect {
    let mut tasks = TASKS.lock().unwrap();
    if let Some(index) = tasks.iter().position(|task| task.id == id) {
        tasks.remove(index);
    }

    Redirect::to("/")
}

 All endpoints are added now it's time to mount them using the Rocket framework. To achieve this add the following function in your code.

fn rocket() -> rocket::Rocket {
    rocket::ignite()
        .manage(TASKS.clone())  // Share the tasks vector across Rocket instances
        .mount("/", routes![index, create,remove])
        .attach(Template::fairing())
}

And call this function into the main function.

fn main() {
    rocket().launch();
}

Start the application by running the following command.

cargo run

Output

todo-cmd-output

Open any browser and navigate to localhost:8000. The output will be as follows.

Output

Tasks

Now enter your new task and click on the add button to add it. You can mark as done your listed tasks and remove them.

Conclusion

To sum it up, this article helps you get good at Rust by guiding you through building a To-Do app. We explored everything step by step, making it easy for beginners. You'll not just learn Rust but also how to make cool stuff on the web.


Similar Articles