こんにちは!大垣です!
前回に引き続き、RustでCRUD処理を作って行きたいと思います。
今回はフロントエンド(UI)の基礎部分を作っていきます。
使用するクレート
・dioxus
・dioxus_router
・dioxus_web
・gloo
・gloo_events
・gloo_net
・serde
・serde_json
・web_sys
・wasm_bindgen
・wasm_bindgen_futures
プロジェクト作成
バックエンドと別プロジェクトとして作成する為、
rustsourceフォルダ内にfrontフォルダを作成し、
その中にプログラムを作成します。
コマンドプロンプトを開き、以下のコマンドを入力します。
docker ps
※rust-rust-appとなっている左のIDを確認する
docker exec -it ↑で確認したID bash
cargo new front
クレートの追加
プロジェクトに必要なクレートを追加するために、Cargo.tomlを編集します。
[dependencies]以降を以下の通りに記述します。
[dependencies]
dioxus = "0.3.2"
dioxus-router = "0.3.0"
dioxus-web = "0.3.1"
gloo = "0.8"
gloo-events = "0.1.2"
gloo-net = "0.2.6"
serde = { version = "1.0.152", features=["derive"] }
serde_json = "1.0.93"
web-sys = "0.3.61"
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.34"
ファイル構成
今回は、以下のファイル構成にします。
📁front
├📁src
│├📁components
││├📝error404.rs
││├📝mod.rs
││├📝task.rs
││└📝top.rs
│└📝main.rs
├📝.gitignore
├📝Cargo.toml
└📝index.html
index.htmlの作成
index.htmlを作成し、以下の内容を記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" />
</head>
<body>
<div id="main"></div>
</body>
</html>
src/main.rsの編集
src/main.rsファイルを編集し、以下の内容で自動作成されるプログラムをすべて削除します。
fn main() {
println!("Hello, world!");
}
削除後、以下の内容を記述します。
// コンポーネント読み込み
mod components;
// クレート宣言
use components::*;
use dioxus::prelude::*;
use dioxus_router::{Route, Router};
use dioxus_web::launch;
/// メイン処理
fn main() {
// アプリ起動
launch(app);
}
/// アプリコンポーネント
fn app(cx: Scope) -> Element {
// レンダリング
cx.render(rsx! {
// ルーティング
Router {
Route { to: "/task", task::task_list {} },
Route { to: "/", top::top {} },
Route { to: "", error404::error404 {} },
}
})
}
src/components/mod.rsの作成
srcフォルダ内に、componentsフォルダを作成します。
作成したフォルダ内に、mod.rsファイルを作成し、以下の内容を記述します。
// 各ファイル読み込み
pub mod error404;
pub mod task;
pub mod top;
src/components/error404.rsの作成
src/components/error404.rsファイルを作成し、以下の内容を記述します。
// クレート宣言
use dioxus::prelude::*;
// 404エラーコンポーネント
pub fn error404(cx: Scope) -> Element {
// レンダリング
cx.render(rsx! {
div {
// コンポーネント全体のクラス
class: "h-full",
p {
class: "justify-center w-full py-10 text-4xl",
"404 Not Found!"
}
}
})
}
src/components/top.rsの作成
src/components/top.rsファイルを作成し、以下の内容を記述します。
// クレート宣言
use dioxus::prelude::*;
use dioxus_router::Link;
/// トップページ
pub fn top(cx: Scope) -> Element {
// レンダリング
cx.render(rsx! {
div {
class: "h-full",
div {
class: "text-center bg-yellow-500 text-black text-4xl",
"タスク管理システム"
}
div {
class: "flex items-center justify-center w-full p-5",
ul {
li {
class: "text-center py-8",
Link {
class: "text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
to: "/task", "タスク一覧"
}
}
}
}
}
})
}
src/components/task.rsの作成
src/components/task.rsファイルを作成し、以下の内容を記述します。
// クレート宣言
use dioxus::prelude::*;
use dioxus_router::Link;
use gloo_events::EventListener;
use gloo_net::http::Request;
use serde::{Deserialize, Serialize};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::*;
use web_sys::{window, HtmlInputElement, HtmlTextAreaElement};
/// タスク構造体
#[derive(Serialize, Deserialize)]
struct Task {
taskid: String,
userid: String,
title: String,
description: String,
}
// モード列挙型
enum Mode {
Add,
Update,
Delete
}
/// タスク一覧
pub fn task_list(cx: Scope) -> Element {
// APIのURL
const API_URL: &str = "http://localhost:9000/task";
// データロード
fn load_task() {
// 非同期処理
spawn_local(async move {
// 取得結果を格納
let task_data = Request::get(API_URL)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
// 取得結果をJsonにパース
let task_json: Vec<Task> = serde_json::from_str(&task_data).unwrap();
// 出力先の要素を取得
let document = window().unwrap().document().unwrap();
let tbody = document.get_element_by_id("tasklist").unwrap();
let remove_element = document.query_selector_all("#tasklist tr").unwrap();
// 既存の行要素を削除
for i in 0..remove_element.length() {
let remove_item = remove_element.item(i);
if let Some(..) = remove_item {
tbody.remove_child(&remove_item.unwrap()).unwrap();
}
}
// 取得データ数分繰り返す
for task in &task_json {
// 各データを退避
let task_id_del = task.taskid.clone();
let user_id_del = task.userid.clone();
let title_del = task.title.clone();
let description_del = task.description.clone();
let task_id_mod = task.taskid.clone();
let user_id_mod = task.userid.clone();
let title_mod = task.title.clone();
let description_mod = task.description.clone();
// 要素生成
let tr = document.create_element("tr").unwrap();
let taskid = document.create_element("td").unwrap();
let userid = document.create_element("td").unwrap();
let title = document.create_element("td").unwrap();
let mod_column = document.create_element("td").unwrap();
let del_column = document.create_element("td").unwrap();
let mod_button = document.create_element("button").unwrap();
let del_button = document.create_element("button").unwrap();
// 値をテキストとして各要素にセット
taskid.set_text_content(Some(&task.taskid));
userid.set_text_content(Some(&task.userid));
title.set_text_content(Some(&task.title));
mod_button.set_text_content(Some("変更"));
del_button.set_text_content(Some("削除"));
// クラスをセット
mod_button.set_class_name("text-2xl inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300");
del_button.set_class_name("text-2xl inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300");
// 行要素を組み立てる
mod_column.append_child(&mod_button).unwrap();
del_column.append_child(&del_button).unwrap();
tr.append_child(&taskid).unwrap();
tr.append_child(&userid).unwrap();
tr.append_child(&title).unwrap();
tr.append_child(&mod_column).unwrap();
tr.append_child(&del_column).unwrap();
// 変更ボタンイベント
let on_click_mod_modal = EventListener::new(&mod_button, "click", move |_event| {
// モーダル表示
modal_show(
Mode::Update,
Task {
taskid: task_id_mod.clone(),
userid: user_id_mod.clone(),
title: title_mod.clone(),
description: description_mod.clone()
}
);
});
// 削除ボタンイベント
let on_click_del_modal = EventListener::new(&del_button, "click", move |_event| {
// モーダル表示
modal_show(
Mode::Delete,
Task {
taskid: task_id_del.clone(),
userid: user_id_del.clone(),
title: title_del.clone(),
description: description_del.clone()
}
);
});
// ボタンイベント有効化
on_click_mod_modal.forget();
on_click_del_modal.forget();
// テーブルに行要素を登録
tbody.append_child(&tr).unwrap();
}
});
}
// 初回のデータロード
load_task();
// 新規登録ボタンイベント
let on_click_add_modal = move |_: MouseEvent| {
// モーダル表示
modal_show(
Mode::Add,
Task {
taskid: String::from(""),
userid: String::from(""),
title: String::from(""),
description: String::from("")
}
);
};
// 登録ボタンイベント
let on_click_add = move |_: MouseEvent| {
// 画面から入力データ取得
let input_data = get_input_data();
// 登録メッセージ表示
set_message(Mode::Add, input_data.taskid.clone());
// APIコール
spawn_local(async move {
Request::put(API_URL)
.body(JsValue::from_str(&format!(
"{}{}{}{}{}{}{}{}",
"taskid=",
&input_data.taskid,
"&userid=",
&input_data.userid,
"&title=",
&input_data.title,
"&description=",
&input_data.description,
)))
.send()
.await
.unwrap();
// 画面リロード
load_task();
});
// モーダル非表示
modal_hide();
};
// 更新ボタンイベント
let on_click_mod = move |_: MouseEvent| {
// 画面から入力データ取得
let input_data = get_input_data();
// 登録メッセージ表示
set_message(Mode::Update, input_data.taskid.clone());
// APIコール
spawn_local(async move {
Request::post(API_URL)
.body(JsValue::from_str(&format!(
"{}{}{}{}{}{}",
"taskid=", &input_data.taskid, "&title=", &input_data.title, "&description=", &input_data.description,
)))
.send()
.await
.unwrap();
// 画面リロード
load_task();
});
// モーダル非表示
modal_hide();
};
// 削除ボタンイベント
let on_click_del = move |_: MouseEvent| {
// 画面から入力データ取得
let input_data = get_input_data();
// 登録メッセージ表示
set_message(Mode::Delete, input_data.taskid.clone());
// APIコール
spawn_local(async move {
Request::delete(API_URL)
.body(JsValue::from_str(&format!("{}{}", "taskid=", &input_data.taskid,)))
.send()
.await
.unwrap();
// 画面リロード
load_task();
});
// モーダル非表示
modal_hide();
};
// 閉じるボタンイベント
let on_click_close = move |_: MouseEvent| {
// モーダル非表示
modal_hide();
};
// メッセージ表示
fn set_message(mode: Mode, task_id: String){
// 要素を取得
let document = window().unwrap().document().unwrap();
let message = document.get_element_by_id("message").unwrap();
// モードで分岐
match mode {
// 新規登録
Mode::Add => {
// 登録メッセージ表示
message.set_text_content(Some(&format!("{}{}{}", "タスク:", &task_id, "が登録されました")));
},
// 変更
Mode::Update => {
// 更新メッセージ表示
message.set_text_content(Some(&format!("{}{}{}", "タスク:", &task_id, "が更新されました")));
},
// 削除
Mode::Delete => {
// 削除メッセージ表示
message.set_text_content(Some(&format!("{}{}{}", "タスク:", &task_id, "が削除されました")));
}
}
}
// モーダル表示処理
fn modal_show(mode: Mode, default_val: Task){
// 要素を取得
let document = window().unwrap().document().unwrap();
let mod_modal = document.get_element_by_id("mod_modal").unwrap();
let mod_modal_title = document.get_element_by_id("mod_modal_title").unwrap();
let add_task = document.get_element_by_id("add_task_button").unwrap();
let mod_task = document.get_element_by_id("mod_task_button").unwrap();
let del_task = document.get_element_by_id("del_task_button").unwrap();
let message = document.get_element_by_id("message").unwrap();
let task_id = document
.get_element_by_id("taskid")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
let user_id = document
.get_element_by_id("userid")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
let title = document
.get_element_by_id("title")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
let description = document
.get_element_by_id("description")
.unwrap()
.dyn_into::<HtmlTextAreaElement>()
.unwrap();
// メッセージ非表示
message.set_text_content(Some(""));
// 値をセット
task_id.set_value(&default_val.taskid);
user_id.set_value(&default_val.userid);
title.set_value(&default_val.title);
description.set_value(&default_val.description);
// モーダル表示
mod_modal.set_class_name("flex h-full w-full z-[1] top-0 bottom-0 left-0 absolute bg-black bg-opacity visible");
// ボタン表示制御
add_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
mod_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
del_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
// モードで分岐
match mode {
// 新規登録
Mode::Add => {
// 登録ボタン有効化
add_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 visible");
// モーダルのタイトルセット
mod_modal_title.set_text_content(Some("新規登録"));
// 操作可否制御
task_id.remove_attribute("disabled").unwrap();
user_id.remove_attribute("disabled").unwrap();
title.remove_attribute("disabled").unwrap();
description.remove_attribute("disabled").unwrap();
},
// 変更
Mode::Update => {
// 更新ボタン有効化
mod_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 visible");
// モーダルのタイトルセット
mod_modal_title.set_text_content(Some("変更"));
// 操作可否制御
task_id.set_attribute("disabled", "disabled").unwrap();
user_id.set_attribute("disabled", "disabled").unwrap();
title.remove_attribute("disabled").unwrap();
description.remove_attribute("disabled").unwrap();
},
// 削除
Mode::Delete => {
// 削除ボタン有効化
del_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 visible");
// モーダルのタイトルセット
mod_modal_title.set_text_content(Some("削除"));
// 操作可否制御
task_id.set_attribute("disabled", "disabled").unwrap();
user_id.set_attribute("disabled", "disabled").unwrap();
title.set_attribute("disabled", "disabled").unwrap();
description.set_attribute("disabled", "disabled").unwrap();
}
}
}
// モーダル非表示処理
fn modal_hide() {
// 要素を取得
let document = window().unwrap().document().unwrap();
let mod_modal = document.get_element_by_id("mod_modal").unwrap();
let add_task = document.get_element_by_id("add_task_button").unwrap();
let mod_task = document.get_element_by_id("mod_task_button").unwrap();
let del_task = document.get_element_by_id("del_task_button").unwrap();
// モーダル非表示
mod_modal.set_class_name(
"flex h-full w-full z-[1] top-0 bottom-0 left-0 absolute bg-black bg-opacity invisible",
);
// ボタン表示制御
add_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
mod_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
del_task.set_class_name("m-5 text-2xl inline-flex item-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 invisible");
}
// 画面要素から入力データ取得
fn get_input_data() -> Task {
// 要素を取得
let document = window().unwrap().document().unwrap();
let task_id = document
.get_element_by_id("taskid")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap().value();
let user_id = document
.get_element_by_id("userid")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap().value();
let task_title = document
.get_element_by_id("title")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap().value();
let task_description = document
.get_element_by_id("description")
.unwrap()
.dyn_into::<HtmlTextAreaElement>()
.unwrap().value();
// タスクデータを返す
Task {
taskid: task_id,
userid: user_id,
title: task_title,
description: task_description
}
}
// レンダリング
cx.render(rsx! {
div {
class: "h-full",
div {
class: "text-center bg-yellow-500 text-black text-4xl",
"タスク一覧"
}
div {
id: "message",
class: "text-red-500 text-2xl text-center",
}
div {
class: "w-full",
div {
class: "overflow-y-scroll top-0",
style: "height:80vh",
table {
class: "border-2 w-full text-2xl text-gray-500 text-center",
thead {
class: "border-2 top-0 w-full sticky bg-blue-400",
tr {
th { class: "px-6 py-3", "タスクID" },
th { class: "px-6 py-3", "ユーザID" },
th { class: "px-6 py-3", "タイトル" },
th { class: "px-6 py-3", "変更" },
th { class: "px-6 py-3", "削除" }
}
},
tbody {
id: "tasklist",
class: "w-full",
}
}
}
}
div {
class: "text-center w-full bottom-0 absolute",
Link {
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
to: "/", "戻る"
}
div {
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
onclick: on_click_add_modal,
"新規登録"
}
}
}
// 登録・変更モーダル
div {
id: "mod_modal",
class: "flex h-full w-full z-[1] top-0 bottom-0 left-0 absolute bg-black bg-opacity-50 invisible",
div {
class: "w-1/2 z-[2] h-4/5 mt-5 bg-white justify-between container mx-auto",
div {
id: "mod_modal_title",
class: "h-8 bg-blue-700 text-2xl text-white text-center",
"登録"
}
div {
class: "grid gap-6 mb-2",
div {
class: "justify-center text-center",
span {
class: "block text-xl text-gray-900",
"タスクID"
input {
"type": "text",
id: "taskid",
class: "bg-gray-50 mt-4 m-1 border border-gray-300 text-gray-900 text-xl rounded-lg focus:ring-blue-500 p-2.5",
value: ""
}
}
}
}
div {
class: "grid gap-6 mb-2",
div {
class: "justify-center text-center",
span {
class: "block text-xl text-gray-900",
"ユーザID"
input {
"type": "text",
id: "userid",
class: "bg-gray-50 mt-4 m-1 border border-gray-300 text-gray-900 text-xl rounded-lg focus:ring-blue-500 p-2.5",
value: ""
}
}
}
}
div {
class: "grid gap-6 mb-2",
div {
class: "justify-center text-center",
span {
class: "block text-xl text-gray-900",
"件名"
input {
"type": "text",
id: "title",
class: "bg-gray-50 mt-4 m-1 border border-gray-300 text-gray-900 text-xl rounded-lg focus:ring-blue-500 p-2.5",
value: ""
}
}
}
}
div {
class: "grid gap-6 mb-2",
div {
class: "justify-center text-center",
span {
class: "block text-xl text-gray-900",
"内容"
textarea {
id: "description",
class: "bg-gray-50 mt-4 m-1 border border-gray-300 text-gray-900 text-xl rounded-lg focus:ring-blue-500 p-2.5",
value: ""
}
}
}
}
div {
class: "justify-center text-center",
button {
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
onclick: on_click_close,
"閉じる"
}
button {
id: "del_task_button",
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
onclick: on_click_del,
"削除"
}
button {
id: "mod_task_button",
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
onclick: on_click_mod,
"更新"
}
button {
id: "add_task_button",
class: "justify-cennter mx-5 text-2xl inline-flex items-center px-3 py-2 font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300",
onclick: on_click_add,
"登録"
}
}
}
}
})
}
動作確認
ここまで作成できたら、コマンドプロンプトを開き、以下のコマンドを実行します。
コマンドを実行することで、作成したWeb画面が起動します。
docker ps
※rust-rust-appとなっている行のCONTAINER IDを確認する
docker exec -it ↑で確認したID bash
cd front
trunk serve --port 9001 --address 0.0.0.0
初回起動時は、各クレートがダウンロードされるため、少し時間がかかります。
「server listening at http://0.0.0.0:9001」と表示されると、起動は成功です。
途中でエラーが出た場合は、プログラムに入力間違い等がないか確認しましょう。
起動完了後、Webブラウザを起動し、「http://localhost:9001」にアクセスします。
以下のような画面が表示されれば、アクセスに成功しています。
新規登録の動作確認
ブラウザに表示された画面から、タスク一覧ボタンを押します。
以下のような画面が表示されるので、新規登録ボタンを押します。
以下のような画面が表示されるので、入力項目をすべて埋めて、登録ボタンを押します。
以下のように表示されれば、登録処理が正常に行われています。
変更の動作確認
先ほど登録を行ったタスクの変更ボタンを押します。
件名、内容を変更し、更新ボタンを押します。
変更の場合は、タスクID、ユーザIDは変更できないようになっています。
以下のように表示されれば、変更処理が正常に行われています。
削除の動作確認
先ほど変更を行ったタスクの削除ボタンを押します。
削除ボタンを押します。
削除の場合はすべての項目が編集できないようになっています。
以下のように表示されれば、削除処理が正常に行われています。
まとめ
以上で、データの登録、変更、削除を行う画面ができました。
次回は、この画面にログインの機能や、入力チェックの機能などを追加してみたいと思います。
次回に続く。
コメント