Skip to content

Commit 93e1842

Browse files
committed
158-jumping-cursor
1 parent 0f68111 commit 93e1842

File tree

11 files changed

+393
-363
lines changed

11 files changed

+393
-363
lines changed

Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ features = [
5252
"HtmlCollection",
5353
"HtmlInputElement",
5454
"HtmlMenuItemElement",
55+
"HtmlProgressElement",
56+
"HtmlOptionElement",
57+
"HtmlDataElement",
58+
"HtmlMeterElement",
59+
"HtmlLiElement",
60+
"HtmlOutputElement",
61+
"HtmlParamElement",
5562
"HtmlTextAreaElement",
5663
"HtmlSelectElement",
5764
"HtmlButtonElement",

examples/server_integration/Cargo.lock

+72-125
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/server_integration/client/Cargo.toml

+12
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,15 @@ wasm-bindgen = "^0.2.45"
1515
futures = "^0.1.27"
1616

1717
shared = { path = "../shared"}
18+
19+
[dependencies.web-sys]
20+
version = "^0.3.25"
21+
features = [
22+
"Blob",
23+
"Event",
24+
"EventTarget",
25+
"File",
26+
"FileList",
27+
"FormData",
28+
"HtmlInputElement",
29+
]

examples/server_integration/client/src/example_e.rs

+128-29
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
11
use futures::Future;
22
use seed::fetch;
33
use seed::prelude::*;
4-
use serde::Serialize;
54
use std::mem;
5+
use wasm_bindgen::JsCast;
6+
use web_sys::{
7+
self,
8+
console::{log_1, log_2},
9+
File,
10+
};
611

712
pub const TITLE: &str = "Example E";
813
pub const DESCRIPTION: &str =
9-
"Write something and click button 'Submit`. See console.log for more info. \
10-
It sends form to the server and server returns 200 OK with 2 seconds delay.";
14+
"Fill form and click 'Submit` button. Server echoes the form back. See console log for more info.";
1115

1216
fn get_request_url() -> String {
1317
"/api/form".into()
1418
}
1519

1620
// Model
1721

18-
#[derive(Serialize, Default)]
22+
#[derive(Default, Debug)]
1923
pub struct Form {
20-
text: String,
21-
checked: bool,
24+
title: String,
25+
description: String,
26+
file: Option<File>,
27+
answer: bool,
28+
}
29+
30+
impl Form {
31+
fn to_form_data(&self) -> Result<web_sys::FormData, JsValue> {
32+
let form_data = web_sys::FormData::new()?;
33+
form_data.append_with_str("title", &self.title)?;
34+
form_data.append_with_str("description", &self.description)?;
35+
if let Some(file) = &self.file {
36+
form_data.append_with_blob("file", file)?;
37+
}
38+
form_data.append_with_str("answer", &self.answer.to_string())?;
39+
Ok(form_data)
40+
}
2241
}
2342

2443
pub enum Model {
@@ -28,7 +47,12 @@ pub enum Model {
2847

2948
impl Default for Model {
3049
fn default() -> Self {
31-
Model::ReadyToSubmit(Form::default())
50+
Model::ReadyToSubmit(Form {
51+
title: "I'm title".into(),
52+
description: "I'm description".into(),
53+
file: None,
54+
answer: true,
55+
})
3256
}
3357
}
3458

@@ -49,25 +73,33 @@ impl Model {
4973

5074
#[derive(Clone)]
5175
pub enum Msg {
52-
TextChanged(String),
53-
CheckedChanged,
76+
TitleChanged(String),
77+
DescriptionChanged(String),
78+
FileChanged(Option<File>),
79+
AnswerChanged,
5480
FormSubmitted(String),
55-
ServerResponded(fetch::ResponseResult<()>),
81+
ServerResponded(fetch::ResponseDataResult<String>),
5682
}
5783

5884
pub fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
5985
match msg {
60-
Msg::TextChanged(text) => model.form_mut().text = text,
61-
Msg::CheckedChanged => toggle(&mut model.form_mut().checked),
86+
Msg::TitleChanged(title) => model.form_mut().title = title,
87+
Msg::DescriptionChanged(description) => model.form_mut().description = description,
88+
Msg::FileChanged(file) => {
89+
model.form_mut().file = file;
90+
}
91+
Msg::AnswerChanged => toggle(&mut model.form_mut().answer),
6292
Msg::FormSubmitted(id) => {
6393
let form = take(model.form_mut());
6494
orders.perform_cmd(send_request(&form));
6595
*model = Model::WaitingForResponse(form);
66-
log!("Form with id", id, "submitted.");
96+
log!(format!("Form {} submitted.", id));
6797
}
68-
Msg::ServerResponded(Ok(_)) => {
98+
Msg::ServerResponded(Ok(response_data)) => {
6999
*model = Model::ReadyToSubmit(Form::default());
70-
log!("Form processed successfully.");
100+
clear_file_input();
101+
log_2(&"%cResponse data:".into(), &"background: yellow".into());
102+
log_1(&response_data.into());
71103
}
72104
Msg::ServerResponded(Err(fail_reason)) => {
73105
*model = Model::ReadyToSubmit(take(model.form_mut()));
@@ -79,8 +111,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
79111
fn send_request(form: &Form) -> impl Future<Item = Msg, Error = Msg> {
80112
fetch::Request::new(get_request_url())
81113
.method(fetch::Method::Post)
82-
.send_json(form)
83-
.fetch(|fetch_object| Msg::ServerResponded(fetch_object.response()))
114+
.body(form.to_form_data().unwrap().into())
115+
.fetch_string_data(Msg::ServerResponded)
116+
}
117+
118+
#[allow(clippy::option_map_unit_fn)]
119+
fn clear_file_input() {
120+
seed::document()
121+
.get_element_by_id("form-file")
122+
.and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
123+
.map(|file_input| {
124+
// Note: `file_input.set_files(None)` doesn't work
125+
file_input.set_value("")
126+
});
84127
}
85128

86129
fn take<T: Default>(source: &mut T) -> T {
@@ -93,29 +136,85 @@ fn toggle(value: &mut bool) {
93136

94137
// View
95138

139+
fn view_form_field(label: Node<Msg>, control: Node<Msg>) -> Node<Msg> {
140+
div![
141+
style! {
142+
"margin-bottom" => unit!(7, px),
143+
"display" => "flex",
144+
},
145+
label.add_style("margin-right", unit!(7, px)),
146+
control
147+
]
148+
}
149+
96150
pub fn view(model: &Model) -> impl View<Msg> {
97151
let btn_disabled = match model {
98-
Model::ReadyToSubmit(form) if !form.text.is_empty() => false,
152+
Model::ReadyToSubmit(form) if !form.title.is_empty() => false,
99153
_ => true,
100154
};
101155

102156
let form_id = "A_FORM".to_string();
103157
form![
158+
style! {
159+
"display" => "flex",
160+
"flex-direction" => "column",
161+
},
104162
raw_ev(Ev::Submit, move |event| {
105163
event.prevent_default();
106164
Msg::FormSubmitted(form_id)
107165
}),
108-
input![
109-
input_ev(Ev::Input, Msg::TextChanged),
110-
attrs! {At::Value => model.form().text}
111-
],
112-
input![
113-
simple_ev(Ev::Click, Msg::CheckedChanged),
114-
attrs! {
115-
At::Type => "checkbox",
116-
At::Checked => model.form().checked.as_at_value(),
117-
}
118-
],
166+
view_form_field(
167+
label!["Title:", attrs! {At::For => "form-title" }],
168+
input![
169+
input_ev(Ev::Input, Msg::TitleChanged),
170+
attrs! {
171+
At::Id => "form-title",
172+
At::Value => model.form().title,
173+
At::Required => true.as_at_value(),
174+
}
175+
]
176+
),
177+
view_form_field(
178+
label!["Description:", attrs! {At::For => "form-description" }],
179+
textarea![
180+
input_ev(Ev::Input, Msg::DescriptionChanged),
181+
attrs! {
182+
At::Id => "form-description",
183+
At::Value => model.form().description,
184+
At::Rows => 1,
185+
},
186+
],
187+
),
188+
view_form_field(
189+
label!["Text file:", attrs! {At::For => "form-file" }],
190+
input![
191+
raw_ev(Ev::Change, |event| {
192+
let file = event
193+
.target()
194+
.and_then(|target| target.dyn_into::<web_sys::HtmlInputElement>().ok())
195+
.and_then(|file_input| file_input.files())
196+
.and_then(|file_list| file_list.get(0));
197+
198+
Msg::FileChanged(file)
199+
}),
200+
attrs! {
201+
At::Type => "file",
202+
At::Id => "form-file",
203+
At::Accept => "text/plain",
204+
}
205+
],
206+
),
207+
view_form_field(
208+
label!["Do you like cocoa?:", attrs! {At::For => "form-answer" }],
209+
input![
210+
simple_ev(Ev::Click, Msg::AnswerChanged),
211+
attrs! {
212+
At::Type => "checkbox",
213+
At::Id => "form-answer",
214+
At::Checked => model.form().answer.as_at_value(),
215+
}
216+
],
217+
),
119218
button![
120219
style! {
121220
"padding" => format!{"{} {}", px(2), px(12)},

examples/server_integration/server/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition = "2018"
88
actix = "0.8.3"
99
actix-web = "1.0.0"
1010
actix-files = "0.1.1"
11+
actix-multipart = "0.1.2"
1112
tokio-timer = "0.2.11"
1213

1314
shared = { path = "../shared" }

examples/server_integration/server/src/main.rs

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use actix::prelude::*;
22
use actix_files::{Files, NamedFile};
3+
use actix_multipart::{Multipart, MultipartError};
34
use actix_web::{get, post, web, App, HttpServer};
5+
use std::fmt::Write;
46
use std::time;
57
use tokio_timer;
68

@@ -37,8 +39,36 @@ fn delayed_response(
3739
}
3840

3941
#[post("form")]
40-
fn form() -> impl Future<Item = (), Error = tokio_timer::Error> {
41-
tokio_timer::sleep(time::Duration::from_millis(2_000)).and_then(move |()| Ok(()))
42+
fn form(form: Multipart) -> impl Future<Item = String, Error = MultipartError> {
43+
form.map(|field| {
44+
// get field name
45+
let name = field
46+
.content_disposition()
47+
.and_then(|cd| cd.get_name().map(ToString::to_string))
48+
.expect("Can't get field name!");
49+
50+
field
51+
// get field value stream
52+
.fold(Vec::new(), |mut value, bytes| -> Result<Vec<u8>, MultipartError> {
53+
for byte in bytes {
54+
value.push(byte)
55+
}
56+
Ok(value)
57+
})
58+
.map(|value| String::from_utf8_lossy(&value).into_owned())
59+
// add name into stream
60+
.map(move |value| (name, value))
61+
.into_stream()
62+
})
63+
.flatten()
64+
.fold(
65+
String::new(),
66+
|mut output, (name, value)| -> Result<String, MultipartError> {
67+
writeln!(&mut output, "{}: {}", name, value).unwrap();
68+
writeln!(&mut output, "___________________").unwrap();
69+
Ok(output)
70+
},
71+
)
4272
}
4373

4474
struct State {

0 commit comments

Comments
 (0)