Search⌘ K
AI Features

Create and Edit the Page

Explore how to set up and implement page creation and editing in Rust using the Yew framework. Learn to manage forms, send API requests with fetch, and apply CSS frameworks like Bootstrap to enhance your front-end design.

We'll cover the following...

Initial setup

We need to create our form to add new products and edit them. But first, we need a few dependencies in our project. Add the next lines to the Cargo.toml file.

Markdown
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"

We want our front-end to look good, so we might add a CSS framework. We can use bootstrap for that. Add the next line to the index.html file.

HTML
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">

Next, we add a few structs to handle the parameters needed to send through the API to our backend server. Remember that the create endpoint needs a NewCompleteProduct, so we need to replicate the same in our front-end.

Rust 1.40.0
#[derive(Serialize, Default, Clone)]
pub struct FormProduct {
pub id: Option<i64>,
pub name: String,
pub cost: f64,
pub active: bool,
}
#[derive(Serialize, Clone)]
pub struct FormVariant {
pub id: Option<i64>,
pub name: String,
}
#[derive(Serialize, Clone)]
pub struct FormVariantValue {
pub variant: FormVariant,
pub values: Vec<Option<String>>
}
#[derive(Serialize, Default, Clone)]
pub struct FormCompleteProduct {
pub product: FormProduct,
pub variants: Vec<FormVariantValue>
}

We also need to create the Ajax request. We can use the fetch API provided from our browser, which is also available as a service in Yew.

Rust 1.40.0
const API_URL: &str = "http://localhost:9000";
fn create_product(product: FormCompleteProduct, callback: Callback<Response<Json<Result<String, Error>>>>) -> FetchTask {
let json_product = &json!(product);
let post_request = Request::post(format!("{}/products", API_URL))
.header("Content-Type", "application/json")
.body(Json(json_product))
.expect("Failed to build request.");
FetchService::fetch(post_request, callback).expect("failed to start request")
}

The first parameter is product with all the data needed, and the second one is callback that handles our response.

Component

use yew::callback::Callback;
use yew::services::fetch::{FetchService, FetchTask, Request, Response};
use yew::format::Json;
use serde_json::json;
use serde::Serialize;
use anyhow::Error;
use yew::prelude::{html, InputData, classes, Component, ComponentLink, Html, ShouldRender};

// An enum to list all the available actions
enum Msg {
	UpdateName(String),
	UpdateCost(f64),
	UpdateActive(bool),
	ReceiveResponse(Result<String, anyhow::Error>),
	Submit
}

// The model is where we save our state
struct Model {
    link: ComponentLink<Self>,
	product: FormCompleteProduct,
	fetch_task: Option<FetchTask>,
	result_fetch: String
}

// We need to implement Component to have all the functionalities available
impl Component for Model {
    type Message = Msg;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
			product: FormCompleteProduct::default(),
			fetch_task: None,
			result_fetch: "".to_string()
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Submit => {
				let callback =
                    self.link
                        .callback(|response: Response<Json<Result<String, anyhow::Error>>>| {
                            let Json(data) = response.into_body();
                            Msg::ReceiveResponse(data)
                        });
				self.fetch_task = Some(create_product(self.product.clone(), callback));
            },
			Msg::UpdateActive(active) => self.product.product.active = active,
			Msg::UpdateCost(cost) => self.product.product.cost = cost,
			Msg::UpdateName(name) => self.product.product.name = name,
			Msg::ReceiveResponse(result) => {
				match result {
					Ok(ok_result) => self.result_fetch = ok_result,
					Err(error) => self.result_fetch = error.to_string()
				}
			}
        }
		true
    }

    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        false
    }

    // Our presentation layer
    fn view(&self) -> Html {
        html! {
            <div class={classes!("container")}>
                <div class={classes!("card", "mb-3", "row", "col-lg-3")}>
                    <h5 class={classes!("card-header")}>
                        { "Product" }
                    </h5>
                    <div class={classes!("card-body")}>
                        <form> 
                            <label class={classes!("form-label")} for="product-name">
                                { "Name" }
                            </label>
                            <input
                                id="product-name"
                                class={classes!("form-control")}
                                placeholder="Name"
                                value={self.product.product.name.clone()}
                                oninput={self.link.callback(|e: InputData| Msg::UpdateName(e.value))}
                            />
                            <label class={classes!("form-label")} for="product-cost">
                                { "Cost" }
                            </label>
                            <input
                                id="product-cost"
                                class={classes!("form-control")}
                                placeholder="Cost"
                                value={self.product.product.cost.to_string()}
                                oninput={self.link.callback(|e: InputData| {
                                    let cost = e.value.parse::<f64>().expect("Not a number");
                                    Msg::UpdateCost(cost)
                                })}
                            />
                            <div class={classes!("form-check")}>
                                <input
                                    id="product-active"
                                    class={classes!("form-check-input")}
                                    type="checkbox"
                                    checked={self.product.product.active}
                                    oninput={self.link.callback(|e: InputData| {
                                        let active = e.value.parse::<bool>().expect("Not a boolean");
                                        Msg::UpdateActive(active)
                                    })}
                                />
                                <label class={classes!("form-check-label")} for="product-active">
                                    { "Active" }
                                </label>
                            </div>
                            <button 
                                class={classes!("btn", "btn-primary")} 
                                onclick=self.link.callback(|_| Msg::Submit)>{ "Save" }
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        }
    }
}

fn main() {
    yew::start_app::<Model>();
}
The form component

We use the struct Msg to capture the action from the view. The Model struct is our component that’ll have the update function, which captures the defined interaction from the view function.