1
1
use futures:: Future ;
2
2
use seed:: fetch;
3
3
use seed:: prelude:: * ;
4
- use serde:: Serialize ;
5
4
use std:: mem;
5
+ use wasm_bindgen:: JsCast ;
6
+ use web_sys:: {
7
+ self ,
8
+ console:: { log_1, log_2} ,
9
+ File ,
10
+ } ;
6
11
7
12
pub const TITLE : & str = "Example E" ;
8
13
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." ;
11
15
12
16
fn get_request_url ( ) -> String {
13
17
"/api/form" . into ( )
14
18
}
15
19
16
20
// Model
17
21
18
- #[ derive( Serialize , Default ) ]
22
+ #[ derive( Default , Debug ) ]
19
23
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
+ }
22
41
}
23
42
24
43
pub enum Model {
@@ -28,7 +47,12 @@ pub enum Model {
28
47
29
48
impl Default for Model {
30
49
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
+ } )
32
56
}
33
57
}
34
58
@@ -49,25 +73,33 @@ impl Model {
49
73
50
74
#[ derive( Clone ) ]
51
75
pub enum Msg {
52
- TextChanged ( String ) ,
53
- CheckedChanged ,
76
+ TitleChanged ( String ) ,
77
+ DescriptionChanged ( String ) ,
78
+ FileChanged ( Option < File > ) ,
79
+ AnswerChanged ,
54
80
FormSubmitted ( String ) ,
55
- ServerResponded ( fetch:: ResponseResult < ( ) > ) ,
81
+ ServerResponded ( fetch:: ResponseDataResult < String > ) ,
56
82
}
57
83
58
84
pub fn update ( msg : Msg , model : & mut Model , orders : & mut Orders < Msg > ) {
59
85
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 ) ,
62
92
Msg :: FormSubmitted ( id) => {
63
93
let form = take ( model. form_mut ( ) ) ;
64
94
orders. perform_cmd ( send_request ( & form) ) ;
65
95
* model = Model :: WaitingForResponse ( form) ;
66
- log ! ( "Form with id " , id, "submitted." ) ;
96
+ log ! ( format! ( "Form {} submitted. " , id) ) ;
67
97
}
68
- Msg :: ServerResponded ( Ok ( _ ) ) => {
98
+ Msg :: ServerResponded ( Ok ( response_data ) ) => {
69
99
* 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 ( ) ) ;
71
103
}
72
104
Msg :: ServerResponded ( Err ( fail_reason) ) => {
73
105
* model = Model :: ReadyToSubmit ( take ( model. form_mut ( ) ) ) ;
@@ -79,8 +111,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
79
111
fn send_request ( form : & Form ) -> impl Future < Item = Msg , Error = Msg > {
80
112
fetch:: Request :: new ( get_request_url ( ) )
81
113
. 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
+ } ) ;
84
127
}
85
128
86
129
fn take < T : Default > ( source : & mut T ) -> T {
@@ -93,29 +136,85 @@ fn toggle(value: &mut bool) {
93
136
94
137
// View
95
138
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
+
96
150
pub fn view ( model : & Model ) -> impl View < Msg > {
97
151
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 ,
99
153
_ => true ,
100
154
} ;
101
155
102
156
let form_id = "A_FORM" . to_string ( ) ;
103
157
form ! [
158
+ style! {
159
+ "display" => "flex" ,
160
+ "flex-direction" => "column" ,
161
+ } ,
104
162
raw_ev( Ev :: Submit , move |event| {
105
163
event. prevent_default( ) ;
106
164
Msg :: FormSubmitted ( form_id)
107
165
} ) ,
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
+ ) ,
119
218
button![
120
219
style! {
121
220
"padding" => format!{ "{} {}" , px( 2 ) , px( 12 ) } ,
0 commit comments