Nivethan
Posted on October 25, 2020
Welcome back! Let's set up our roadmap as this chapter will be beginning of passing data back and forth between the website and the web application. This is where things will start to branch out and our file count will increase.
In this chapter we will create a sign up page, a login page, and a submission page that will just print the data to the screen.
Before we begin, lets make one change to our index.html. We are going to take advantage of the fact that tera templates have inheritance and abstract out the standard parts of each of our pages.
Copy index.html to base.html.
./templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
Here we removed our for loop and added block content, this means that in child templates we can set up a block content that will then get placed in the parent template.
./templates/index.html
{% extends "base.html" %}
{% block content %}
{% for post in posts %}
<div>
<a href="{{ post.link }}">{{ post.title }}</a>
<small>{{ post.author }}</small>
</div>
{% endfor %}
{% endblock %}
In index.html we extend base.html and create our block "content". This way our templates will only hold what they need.
Sign Up Page
For our sign up page, we'll keep it very simple, for now no front end validation so we'll just pass everything back to rust. We'll have a username, email and password.
Let's get started.
./templates/signup.html
{% extends "base.html" %}
{% block content %}
<form action="" method="POST">
<div>
<label for="username">Username:</label>
<input type="text" name="username">
</div>
<div>
<label for="email">E-mail:</label>
<input type="email" name="email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password">
</div>
<input type="submit" value="Sign Up">
</form>
{% endblock %}
The sign up page is very bare bones but for now it will suffice. We will submit our form via POST to the page itself.
Now to hook this template up to rust.
./src/main.rs
...
async fn signup(tera: web::Data<Tera>) -> impl Responder {
let mut data = Context::new();
data.insert("title", "Sign Up");
let rendered = tera.render("signup.html", &data).unwrap();
HttpResponse::Ok().body(rendered)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let tera = Tera::new("templates/**/*").unwrap();
App::new()
.data(tera)
.route("/", web::get().to(index))
.route("/signup", web::get().to(signup))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Here the first thing to notice is that in our main function we set up a new route. We have "/signup" on a HTTP Get call our signup function. Our signup function will then set the title and pass the data and the page we want to render to tera.render.
We can now go to 127.0.0.1:8000/signup in our browser and we should see our very nice form!
The next thing we will do is have rust print out what it receives on a HTTP POST to that same page.
./src/main.rs
...
use serde::{Serialize, Deserialize};
...
#[derive(Debug, Deserialize)]
struct User {
username: String,
email: String,
password: String,
}
async fn process_signup(data: web::Form<User>) -> impl Responder {
println!("{:?}", data);
HttpResponse::Ok().body(format!("Successfully saved user: {}", data.username))
}
...
.route("/signup", web::get().to(signup))
.route("/signup", web::post().to(process_signup))
...
The first thing we do is add a new route for the post request and have this route run our process_signup function.
The next thing we need to do is get the data out of our post request and we can do this with the Form utility from actix::web. This will let us get the data out of the request so that we can process it. To extract this data out however we need to do one other thing.
We need to be able take the string data in the post request and convert that into a rust object. For this we want to do the opposite of the serialize we did in the last chapter. We include the Deserialize utility from serde at the very top and then we derive it for our User struct. This struct matches the form we have in our template. We also need to derive Debug so we can print the data out.
We can now go to our signup page and try it out!
We should be able to get our page and once we click submit we should see the following in our terminal.
Finished dev [unoptimized + debuginfo] target(s) in 16.75s
Running `target/debug/hacker-clone`
User { username: "niv", email: "niv@example.com", password: "123" }
!
We're getting somewhere now. We can have our website talk to our rust web application!
Before we wrap up lets finish up the next two pages, our login page and our submission page.
We will create our template, we will add our route, we will create a struct of our form, we will write a rust function.
./templates/login.html
{% extends "base.html" %}
{% block content %}
<form action="" method="POST">
<div>
<label for="username">Username:</label>
<input type="text" name="username">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password">
</div>
<input type="submit" value="Login">
</form>
{% endblock %}
./src/main.rs
...
#[derive(Debug, Deserialize)]
struct LoginUser {
username: String,
password: String,
}
async fn login(tera: web::Data<Tera>) -> impl Responder {
let mut data = Context::new();
data.insert("title", "Login");
let rendered = tera.render("login.html", &data).unwrap();
HttpResponse::Ok().body(rendered)
}
async fn process_login(data: web::Form<LoginUser>) -> impl Responder {
println!("{:?}", data);
HttpResponse::Ok().body(format!("Logged in: {}", data.username))
}
...
...
.route("/signup", web::post().to(process_signup))
.route("/login", web::get().to(login))
.route("/login", web::post().to(process_login))
...
We should now be able to go to 127.0.0.1:8000/login in our browser and log in!
Next up, the submission form! We will follow the same steps.
./templates/submission.html
{% extends "base.html" %}
{% block content %}
<form action="" method="POST">
<div>
<label for="title">Title:</label>
<input type="text" name="title">
</div>
<div>
<label for="link">Link:</label>
<input type="text" name="link">
</div>
<input type="submit" value="Submit">
</form>
{% endblock %}
./src/main.rs
...
#[derive(Debug, Deserialize)]
struct Submission {
title: String,
link: String,
}
async fn submission(tera: web::Data<Tera>) -> impl Responder {
let mut data = Context::new();
data.insert("title", "Submit a Post");
let rendered = tera.render("submission.html", &data).unwrap();
HttpResponse::Ok().body(rendered)
}
async fn process_submission(data: web::Form<Submission>) -> impl Responder {
println!("{:?}", data);
HttpResponse::Ok().body(format!("Posted submission: {}", data.title))
}
...
...
.route("/login", web::post().to(process_login))
.route("/submission", web::get().to(submission))
.route("/submission", web::post().to(process_submission))
...
Now if we navigate to 127.0.0.1:8000/submission in our browser we should be able to test our submission.
In the terminal we should see:
Submission { title: "test", link: "http://123" }
Done! We now have 4 major pages of our website wired to our rust application. We aren't doing much with it yet but this was a pretty big milestone. Next up we'll work on our rust application talking to our database. Exciting!
Posted on October 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.