Skip to content

158-jumping-cursor #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ features = [
"HtmlCollection",
"HtmlInputElement",
"HtmlMenuItemElement",
"HtmlProgressElement",
"HtmlOptionElement",
"HtmlDataElement",
"HtmlMeterElement",
"HtmlLiElement",
"HtmlOutputElement",
"HtmlParamElement",
"HtmlTextAreaElement",
"HtmlSelectElement",
"HtmlButtonElement",
Expand Down
197 changes: 72 additions & 125 deletions examples/server_integration/Cargo.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions examples/server_integration/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ wasm-bindgen = "^0.2.45"
futures = "^0.1.27"

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

[dependencies.web-sys]
version = "^0.3.25"
features = [
"Blob",
"Event",
"EventTarget",
"File",
"FileList",
"FormData",
"HtmlInputElement",
]
157 changes: 128 additions & 29 deletions examples/server_integration/client/src/example_e.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
use futures::Future;
use seed::fetch;
use seed::prelude::*;
use serde::Serialize;
use std::mem;
use wasm_bindgen::JsCast;
use web_sys::{
self,
console::{log_1, log_2},
File,
};

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

fn get_request_url() -> String {
"/api/form".into()
}

// Model

#[derive(Serialize, Default)]
#[derive(Default, Debug)]
pub struct Form {
text: String,
checked: bool,
title: String,
description: String,
file: Option<File>,
answer: bool,
}

impl Form {
fn to_form_data(&self) -> Result<web_sys::FormData, JsValue> {
let form_data = web_sys::FormData::new()?;
form_data.append_with_str("title", &self.title)?;
form_data.append_with_str("description", &self.description)?;
if let Some(file) = &self.file {
form_data.append_with_blob("file", file)?;
}
form_data.append_with_str("answer", &self.answer.to_string())?;
Ok(form_data)
}
}

pub enum Model {
Expand All @@ -28,7 +47,12 @@ pub enum Model {

impl Default for Model {
fn default() -> Self {
Model::ReadyToSubmit(Form::default())
Model::ReadyToSubmit(Form {
title: "I'm title".into(),
description: "I'm description".into(),
file: None,
answer: true,
})
}
}

Expand All @@ -49,25 +73,33 @@ impl Model {

#[derive(Clone)]
pub enum Msg {
TextChanged(String),
CheckedChanged,
TitleChanged(String),
DescriptionChanged(String),
FileChanged(Option<File>),
AnswerChanged,
FormSubmitted(String),
ServerResponded(fetch::ResponseResult<()>),
ServerResponded(fetch::ResponseDataResult<String>),
}

pub fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
match msg {
Msg::TextChanged(text) => model.form_mut().text = text,
Msg::CheckedChanged => toggle(&mut model.form_mut().checked),
Msg::TitleChanged(title) => model.form_mut().title = title,
Msg::DescriptionChanged(description) => model.form_mut().description = description,
Msg::FileChanged(file) => {
model.form_mut().file = file;
}
Msg::AnswerChanged => toggle(&mut model.form_mut().answer),
Msg::FormSubmitted(id) => {
let form = take(model.form_mut());
orders.perform_cmd(send_request(&form));
*model = Model::WaitingForResponse(form);
log!("Form with id", id, "submitted.");
log!(format!("Form {} submitted.", id));
}
Msg::ServerResponded(Ok(_)) => {
Msg::ServerResponded(Ok(response_data)) => {
*model = Model::ReadyToSubmit(Form::default());
log!("Form processed successfully.");
clear_file_input();
log_2(&"%cResponse data:".into(), &"background: yellow".into());
log_1(&response_data.into());
}
Msg::ServerResponded(Err(fail_reason)) => {
*model = Model::ReadyToSubmit(take(model.form_mut()));
Expand All @@ -79,8 +111,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
fn send_request(form: &Form) -> impl Future<Item = Msg, Error = Msg> {
fetch::Request::new(get_request_url())
.method(fetch::Method::Post)
.send_json(form)
.fetch(|fetch_object| Msg::ServerResponded(fetch_object.response()))
.body(form.to_form_data().unwrap().into())
.fetch_string_data(Msg::ServerResponded)
}

#[allow(clippy::option_map_unit_fn)]
fn clear_file_input() {
seed::document()
.get_element_by_id("form-file")
.and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|file_input| {
// Note: `file_input.set_files(None)` doesn't work
file_input.set_value("")
});
}

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

// View

fn view_form_field(label: Node<Msg>, control: Node<Msg>) -> Node<Msg> {
div![
style! {
"margin-bottom" => unit!(7, px),
"display" => "flex",
},
label.add_style("margin-right", unit!(7, px)),
control
]
}

pub fn view(model: &Model) -> impl View<Msg> {
let btn_disabled = match model {
Model::ReadyToSubmit(form) if !form.text.is_empty() => false,
Model::ReadyToSubmit(form) if !form.title.is_empty() => false,
_ => true,
};

let form_id = "A_FORM".to_string();
form![
style! {
"display" => "flex",
"flex-direction" => "column",
},
raw_ev(Ev::Submit, move |event| {
event.prevent_default();
Msg::FormSubmitted(form_id)
}),
input![
input_ev(Ev::Input, Msg::TextChanged),
attrs! {At::Value => model.form().text}
],
input![
simple_ev(Ev::Click, Msg::CheckedChanged),
attrs! {
At::Type => "checkbox",
At::Checked => model.form().checked.as_at_value(),
}
],
view_form_field(
label!["Title:", attrs! {At::For => "form-title" }],
input![
input_ev(Ev::Input, Msg::TitleChanged),
attrs! {
At::Id => "form-title",
At::Value => model.form().title,
At::Required => true.as_at_value(),
}
]
),
view_form_field(
label!["Description:", attrs! {At::For => "form-description" }],
textarea![
input_ev(Ev::Input, Msg::DescriptionChanged),
attrs! {
At::Id => "form-description",
At::Value => model.form().description,
At::Rows => 1,
},
],
),
view_form_field(
label!["Text file:", attrs! {At::For => "form-file" }],
input![
raw_ev(Ev::Change, |event| {
let file = event
.target()
.and_then(|target| target.dyn_into::<web_sys::HtmlInputElement>().ok())
.and_then(|file_input| file_input.files())
.and_then(|file_list| file_list.get(0));

Msg::FileChanged(file)
}),
attrs! {
At::Type => "file",
At::Id => "form-file",
At::Accept => "text/plain",
}
],
),
view_form_field(
label!["Do you like cocoa?:", attrs! {At::For => "form-answer" }],
input![
simple_ev(Ev::Click, Msg::AnswerChanged),
attrs! {
At::Type => "checkbox",
At::Id => "form-answer",
At::Checked => model.form().answer.as_at_value(),
}
],
),
button![
style! {
"padding" => format!{"{} {}", px(2), px(12)},
Expand Down
1 change: 1 addition & 0 deletions examples/server_integration/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2018"
actix = "0.8.3"
actix-web = "1.0.0"
actix-files = "0.1.1"
actix-multipart = "0.1.2"
tokio-timer = "0.2.11"

shared = { path = "../shared" }
34 changes: 32 additions & 2 deletions examples/server_integration/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use actix::prelude::*;
use actix_files::{Files, NamedFile};
use actix_multipart::{Multipart, MultipartError};
use actix_web::{get, post, web, App, HttpServer};
use std::fmt::Write;
use std::time;
use tokio_timer;

Expand Down Expand Up @@ -37,8 +39,36 @@ fn delayed_response(
}

#[post("form")]
fn form() -> impl Future<Item = (), Error = tokio_timer::Error> {
tokio_timer::sleep(time::Duration::from_millis(2_000)).and_then(move |()| Ok(()))
fn form(form: Multipart) -> impl Future<Item = String, Error = MultipartError> {
form.map(|field| {
// get field name
let name = field
.content_disposition()
.and_then(|cd| cd.get_name().map(ToString::to_string))
.expect("Can't get field name!");

field
// get field value stream
.fold(Vec::new(), |mut value, bytes| -> Result<Vec<u8>, MultipartError> {
for byte in bytes {
value.push(byte)
}
Ok(value)
})
.map(|value| String::from_utf8_lossy(&value).into_owned())
// add name into stream
.map(move |value| (name, value))
.into_stream()
})
.flatten()
.fold(
String::new(),
|mut output, (name, value)| -> Result<String, MultipartError> {
writeln!(&mut output, "{}: {}", name, value).unwrap();
writeln!(&mut output, "___________________").unwrap();
Ok(output)
},
)
}

struct State {
Expand Down
Loading