Skip to content

Commit

Permalink
frontend: add form validation
Browse files Browse the repository at this point in the history
  • Loading branch information
hynseok committed Nov 4, 2024
1 parent 36c82e2 commit 469e1ca
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 26 deletions.
150 changes: 124 additions & 26 deletions frontend/src/components/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ pub struct Form {
content: String,
fetch_task: Option<FetchTask>,
loading: bool,
validation_errors: ValidationErrors,
}

#[derive(Default)]
struct ValidationErrors {
name: Option<String>,
email: Option<String>,
content: Option<String>,
}

pub enum Msg {
Expand All @@ -39,57 +47,72 @@ impl Component for Form {
content: String::new(),
fetch_task: None,
loading: false,
validation_errors: ValidationErrors::default(),
}
}

fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::UpdateName(name) => {
self.name = name;
self.validate_name();
true
}
Msg::UpdateEmail(email) => {
self.email = email;
self.validate_email();
true
}
Msg::UpdateContent(content) => {
self.content = content;
self.validate_content();
true
}
Msg::Submit => {
self.loading = true;

let form_data = FormData {
name: self.name.clone(),
email: self.email.clone(),
content: self.content.clone(),
};

let request = Request::post("https://ask.scg.skku.ac.kr/v1/send")
.header("Content-Type", "application/json; charset=utf-8")
.body(Json(&form_data))
.expect("Could not build request.");

let callback = self.link.callback(|response: Response<Nothing>| {
if response.status().is_success() {
Msg::Response(Ok(()))
} else {
Msg::Response(Err(anyhow::anyhow!("Request failed")))
}
});
self.validate_all();

if self.is_valid() && !self.loading {
self.loading = true;

let task = FetchService::fetch(request, callback).expect("Failed to start request");
self.fetch_task = Some(task);
let form_data = FormData {
name: self.name.clone(),
email: self.email.clone(),
content: self.content.clone(),
};

let request = Request::post("https://ask.scg.skku.ac.kr/v1/send")
.header("Content-Type", "application/json; charset=utf-8")
.body(Json(&form_data))
.expect("Could not build request.");

let callback = self.link.callback(|response: Response<Nothing>| {
if response.status().is_success() {
Msg::Response(Ok(()))
} else {
Msg::Response(Err(anyhow::anyhow!("Request failed")))
}
});

let task =
FetchService::fetch(request, callback).expect("Failed to start request");
self.fetch_task = Some(task);
}

false
true
}
Msg::Response(result) => {
self.loading = false;
self.fetch_task = None;
match result {
Ok(_) => log::info!("Request successful"),
Ok(_) => {
log::info!("Request successful");
self.name = String::new();
self.email = String::new();
self.content = String::new();
self.validation_errors = ValidationErrors::default();
}
Err(err) => log::error!("Request failed: {:?}", err),
}
self.fetch_task = None;
true
}
}
Expand All @@ -100,6 +123,8 @@ impl Component for Form {
}

fn view(&self) -> Html {
let is_disabled = self.loading || !self.is_valid();

html! {
<form onsubmit=self.link.callback(|e: FocusEvent| {
e.prevent_default();
Expand All @@ -114,6 +139,13 @@ impl Component for Form {
placeholder="이름을 작성해주세요."
oninput=self.link.callback(|e: InputData| Msg::UpdateName(e.value))
/>
{
if let Some(error) = &self.validation_errors.name {
html! { <p style="color: red;">{error}</p> }
} else {
html! {}
}
}
</div>
<div>
<label for="email">{"이메일:"}</label>
Expand All @@ -124,6 +156,13 @@ impl Component for Form {
value=self.email.clone()
oninput=self.link.callback(|e: InputData| Msg::UpdateEmail(e.value))
/>
{
if let Some(error) = &self.validation_errors.email {
html! { <p style="color: red;">{error}</p> }
} else {
html! {}
}
}
</div>
<div>
<label for="content">{"내용:"}</label>
Expand All @@ -133,9 +172,68 @@ impl Component for Form {
value={self.content.clone()}
oninput=self.link.callback(|e: InputData| Msg::UpdateContent(e.value))
/>
{
if let Some(error) = &self.validation_errors.content {
html! { <p style="color: red;">{error}</p> }
} else {
html! {}
}
}
</div>
<button type="submit" disabled=self.loading>{"문의하기"}</button>
<button type="submit" disabled=is_disabled>
{
if self.loading {
html! {
<>
<span class="spinner"></span>
</>
}
} else {
{ html! { "문의하기" } }
}
}
</button>
</form>
}
}
}

impl Form {
fn validate_name(&mut self) {
if self.name.trim().is_empty() {
self.validation_errors.name = Some("이름을 작성해주세요.".into());
} else {
self.validation_errors.name = None;
}
}

fn validate_email(&mut self) {
if self.email.trim().is_empty() {
self.validation_errors.email = Some("이메일을 작성해주세요.".into());
} else if !self.email.contains("@") {
self.validation_errors.email = Some("올바른 이메일 주소를 입력해주세요.".into());
} else {
self.validation_errors.email = None;
}
}

fn validate_content(&mut self) {
if self.content.trim().is_empty() {
self.validation_errors.content = Some("내용을 작성해주세요.".into());
} else {
self.validation_errors.content = None;
}
}

fn validate_all(&mut self) {
self.validate_name();
self.validate_email();
self.validate_content();
}

fn is_valid(&self) -> bool {
self.validation_errors.name.is_none()
&& self.validation_errors.email.is_none()
&& self.validation_errors.content.is_none()
}
}
19 changes: 19 additions & 0 deletions frontend/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ button[type="submit"]:disabled {
background-color: #9f9f9f;
cursor: not-allowed;
}
button[type="submit"]:disabled:hover {
background-color: #9f9f9f;
}

button[type="submit"]:hover {
background-color: #dddddd;
Expand Down Expand Up @@ -253,4 +256,20 @@ button[type="submit"]:active {

.loading_spinner_text {
margin-top: 10px;
}

.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 2s linear infinite;
display: inline-block;
margin-right: 8px;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

0 comments on commit 469e1ca

Please sign in to comment.