dioxus-forms/src/main.rs

503 lines
14 KiB
Rust

use std::{hash::Hash, net::SocketAddr};
use axum::{
extract::WebSocketUpgrade,
response::{Html, IntoResponse},
routing::get,
Router,
};
use dioxus::prelude::*;
use dioxus_liveview::LiveViewPool;
use dioxus_router::{Link, Redirect, Route, Router};
mod form;
use form::{use_form, Errors, FormError, NumberInput, PasswordInput, TextInput, Textarea};
#[derive(Clone, Debug, Default)]
struct Form {
username: String,
password: String,
password_confirmation: String,
age: f64,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum Field {
Username,
Password,
ConfirmPassword,
Age,
}
#[derive(Debug)]
enum Error {
UsernameBlank,
UsernameTooShort,
PasswordBlank,
PasswordTooShort,
PasswordMismatch,
ParseAge,
AgeTooYoung,
AgeRequired,
}
impl form::Form for Form {
type FieldName = Field;
fn set(&mut self, field_name: Self::FieldName, value: &str, errors: &mut Errors<'_>) {
match field_name {
Field::Username => {
errors
.test(value.is_empty(), Error::UsernameBlank)
.test(value.len() < 5, Error::UsernameTooShort);
self.username = value.to_owned();
}
Field::Password => {
errors
.test(value.is_empty(), Error::PasswordBlank)
.test(value.len() < 8, Error::PasswordTooShort);
self.password = value.to_owned();
}
Field::ConfirmPassword => {
errors.test(value != self.password, Error::PasswordMismatch);
self.password_confirmation = value.to_owned();
}
Field::Age => {
errors.test(value.is_empty(), Error::AgeRequired);
if value.is_empty() {
return;
}
match value.parse() {
Ok(age) => {
errors
.remove_error(&Error::ParseAge)
.test(age < 18.0, Error::AgeTooYoung);
self.age = age;
}
Err(_) => {
errors.remove_error(&Error::AgeTooYoung);
errors.add_error(Error::ParseAge);
}
};
}
}
}
}
impl std::fmt::Display for Field {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Username => write!(f, "username"),
Self::Password => write!(f, "password"),
Self::ConfirmPassword => write!(f, "confirm-password"),
Self::Age => write!(f, "age"),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UsernameBlank => write!(f, "Username is required"),
Self::UsernameTooShort => write!(f, "Username is too short"),
Self::PasswordBlank => write!(f, "Password is required"),
Self::PasswordTooShort => write!(f, "Password is too short"),
Self::PasswordMismatch => write!(f, "Passwords must match"),
Self::ParseAge => write!(f, "Age is invalid"),
Self::AgeTooYoung => write!(f, "You are not old enough"),
Self::AgeRequired => write!(f, "Age is required"),
}
}
}
impl std::error::Error for Error {}
impl FormError for Error {
fn ident(&self) -> &'static str {
match self {
Self::UsernameBlank => "username-blank",
Self::UsernameTooShort => "username-too-short",
Self::PasswordBlank => "password-blank",
Self::PasswordTooShort => "password-too-shorrt",
Self::PasswordMismatch => "password-mismatch",
Self::ParseAge => "parse-age",
Self::AgeTooYoung => "age-too-young",
Self::AgeRequired => "age-required",
}
}
}
#[allow(non_snake_case)]
fn FirstForm(cx: Scope) -> Element {
let form = use_ref(cx, Form::default);
let form_state = use_form(cx, form.clone(), |_| {});
cx.render(rsx! {
div {
class: "form-container",
form {
onsubmit: move |_| {
let valid = form_state.write().validate().is_valid();
let form = form.read();
println!("{form:?} - valid: {valid}");
},
TextInput {
name: Field::Username,
label: cx.render(rsx! { "Username" }),
},
PasswordInput {
name: Field::Password,
label: cx.render(rsx! { "Password" }),
},
PasswordInput {
name: Field::ConfirmPassword,
label: cx.render(rsx! { "Confirm Password" }),
},
NumberInput {
name: Field::Age,
label: cx.render(rsx! { "Age" }),
step: 0.1,
},
input {
r#type: "submit",
value: "Sign Up"
},
},
},
})
}
#[allow(non_snake_case)]
fn SecondForm(cx: Scope) -> Element {
let form = use_ref(cx, Form::default);
let form_state = use_form(cx, form.clone(), |form| {
form.always_show_errors();
});
cx.render(rsx! {
div {
class: "form-container",
form {
onsubmit: move |_| {
let valid = form_state.write().validate().is_valid();
let form = form.read();
println!("{form:?} - valid: {valid}");
},
TextInput {
name: Field::Username,
label: cx.render(rsx! { "Username" }),
},
PasswordInput {
name: Field::Password,
label: cx.render(rsx! { "Password" }),
},
PasswordInput {
name: Field::ConfirmPassword,
label: cx.render(rsx! { "Confirm Password" }),
},
NumberInput {
name: Field::Age,
label: cx.render(rsx! { "Age" }),
step: 0.1,
},
input {
r#type: "submit",
value: "Sign Up"
},
},
}
})
}
#[allow(non_snake_case)]
fn ThirdForm(cx: Scope) -> Element {
let form = use_ref(cx, Form::default);
let form_state = use_form(cx, form.clone(), |form| {
form.disable();
});
cx.render(rsx! {
div {
class: "form-container",
form {
onsubmit: move |_| {
let valid = form_state.write().validate().is_valid();
let form = form.read();
println!("{form:?} - valid: {valid}");
},
TextInput {
name: Field::Username,
label: cx.render(rsx! { "Username" }),
},
PasswordInput {
name: Field::Password,
label: cx.render(rsx! { "Password" }),
},
PasswordInput {
name: Field::ConfirmPassword,
label: cx.render(rsx! { "Confirm Password" }),
},
NumberInput {
name: Field::Age,
label: cx.render(rsx! { "Age" }),
step: 0.1,
},
input {
r#type: "submit",
value: "Sign Up"
},
},
},
})
}
#[allow(non_snake_case)]
fn FourthForm(cx: Scope) -> Element {
cx.render(rsx! {
div {
class: "form-container",
form {
onsubmit: move |event| {
println!("{:?}", event.values);
},
TextInput {
name: Field::Username,
label: cx.render(rsx! { "Username" }),
},
PasswordInput {
name: Field::Password,
label: cx.render(rsx! { "Password" }),
},
PasswordInput {
name: Field::ConfirmPassword,
label: cx.render(rsx! { "Confirm Password" }),
},
NumberInput {
name: Field::Age,
label: cx.render(rsx! { "Age" }),
step: 0.1,
disabled: true,
}
input {
r#type: "submit",
value: "Sign Up"
},
},
}
})
}
#[allow(non_snake_case)]
fn FifthForm(cx: Scope) -> Element {
cx.render(rsx! {
div {
class: "form-container",
form {
onsubmit: move |event| {
println!("{:?}", event.values);
},
Textarea {
name: "post"
label: cx.render(rsx! { "Text" })
},
},
},
})
}
#[allow(non_snake_case)]
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
class: "form-list",
FirstForm {},
SecondForm {},
ThirdForm {},
FourthForm {},
FifthForm {},
}
})
}
fn application(cx: Scope) -> Element {
cx.render(rsx! {
Router {
nav {
div {
Link { to: "/posts", "Home" },
},
},
Route { to: "/posts", Home {} },
Redirect { from: "", to: "/posts" },
}
})
}
async fn spa(addr: SocketAddr) -> Html<String> {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Dioxus LiveView with Axum</title>
<style>
* {{
box-sizing: border-box;
}}
body {{
background-color: #333;
color: #f5f5f5;
font-family: sans;
margin: 0;
min-height: 100vh;
}}
.form-list {{
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}}
.form-container {{
padding: 16px;
max-width: 500px;
width: 100%;
}}
form {{
background-color: #fff;
border: 1px solid #e5e5e5;
border-radius: 8px;
margin: 0 auto;
color: #222;
padding: 16px;
width: 100%;
}}
.form-input {{
display: block;
}}
.form-input--label {{
display: block;
font-size: 14px;
font-weight: 500;
padding: 4px 0;
}}
.form-input input[type=text],
.form-input input[type=password],
.form-input input[type=number],
.form-input textarea {{
border: 1px solid #e5e5e5;
border-radius: 3px;
box-shadow: inset 0 0 4px rgba(50, 50, 50, 0.1);
display: block;
font-size: 16px;
line-height: 22px;
outline: none;
padding: 8px 16px;
width: 100%;
}}
.form-input input[type=text]:hover,
.form-input input[type=password]:hover,
.form-input input[type=number]:hover,
.form-input textarea:hover {{
border-color: #ff84ca;
}}
.form-input input[type=text]:focus,
.form-input input[type=password]:focus,
.form-input input[type=number]:focus,
.form-input textarea:focus {{
border-color: #ff84ca;
box-shadow: inset 0 0 6px rgba(255, 132, 202, 0.4), 0 0 4px rgba(255, 132, 202, 0.2);
}}
.form-input input[type=text].error,
.form-input input[type=password].error,
.form-input input[type=number].error,
.form-input textarea.error {{
border-color: #ca3e3e;
}}
.form-input input[disabled=true],
.form-input textarea[disabled=true] {{
background-color: #f5f5f5;
}}
.form-input textarea {{
min-height: 200px;
}}
.form-input--errors {{
color: #ca3e3e;
list-style: none;
margin: 0;
padding: 4px 0;
}}
.form-input--errors--error {{
font-size: 14px;
padding: 2px 0;
}}
</style>
</head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
// Create the glue code to connect to the WebSocket on the "/ws" route
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
))
}
async fn websocket(ws: WebSocketUpgrade, view: LiveViewPool) -> impl IntoResponse {
ws.on_upgrade(move |socket| async move {
// When the WebSocket is upgraded, launch the LiveView with the app component
_ = view
.launch(dioxus_liveview::axum_socket(socket), application)
.await;
})
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
// The root route contains the glue code to connect to the WebSocket
.route("/", get(move || spa(addr)))
// The WebSocket route is what Dioxus uses to communicate with the browser
.route("/ws", get(move |sock| websocket(sock, view)));
println!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse()?)
.serve(app.into_make_service())
.await?;
Ok(())
}