From b0dabf9b3d28f1dc0b133b904a738018f96f89dd Mon Sep 17 00:00:00 2001 From: Kenzi Connor Date: Fri, 19 Jul 2024 23:24:43 -0700 Subject: [PATCH 1/3] add start of form with file upload example --- examples/form_file/Cargo.toml | 14 +++++ examples/form_file/README.md | 17 +++++ examples/form_file/index.html | 11 ++++ examples/form_file/src/main.rs | 110 +++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 examples/form_file/Cargo.toml create mode 100644 examples/form_file/README.md create mode 100644 examples/form_file/index.html create mode 100644 examples/form_file/src/main.rs diff --git a/examples/form_file/Cargo.toml b/examples/form_file/Cargo.toml new file mode 100644 index 00000000000..2b26ff288b0 --- /dev/null +++ b/examples/form_file/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "form_file" +version = "0.1.0" +authors = ["Kenzi Connor "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +base64 = "0.21.5" +gloo = "0.10" +gloo-console = "0.3.0" +js-sys = "0.3" +web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement"] } +yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/form_file/README.md b/examples/form_file/README.md new file mode 100644 index 00000000000..55b0e4dedf0 --- /dev/null +++ b/examples/form_file/README.md @@ -0,0 +1,17 @@ +# Form /w File Upload Example + +[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fform_file)](https://examples.yew.rs/vform_file) + +This example shows managing a file_data and related form fields together + +## Concepts + +Demonstrates reading from files in Yew with the help of [`gloo::file`](https://docs.rs/gloo-file/latest/gloo_file/). + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/form_file/index.html b/examples/form_file/index.html new file mode 100644 index 00000000000..745fd8c94cd --- /dev/null +++ b/examples/form_file/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Form w/ File Upload + + + + + + diff --git a/examples/form_file/src/main.rs b/examples/form_file/src/main.rs new file mode 100644 index 00000000000..02b79558b0d --- /dev/null +++ b/examples/form_file/src/main.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use gloo::file::{callbacks::FileReader, File}; +use gloo_console::debug; +use web_sys::{FormData, HtmlFormElement, File as RawFile}; +use yew::prelude::*; + +pub struct FileDetails { + name: String, + file_type: String, + data: Vec, +} + +pub enum Msg { + Loaded(FileDetails), + Submit(SubmitEvent), +} + +pub struct App { + readers: HashMap, + files: Vec, +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + Self { + readers: HashMap::default(), + files: Vec::default(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::Loaded(file) => { + let name = file.name.clone(); + self.files.push(file); + self.readers.remove(&name); + } + Msg::Submit(event) => { + debug!(event.clone()); + event.prevent_default(); + let form: HtmlFormElement = event.target_unchecked_into(); + let form_data = FormData::new_with_form(&form).expect("form data"); + let image_file = File::from(RawFile::from(form_data.get("file"))); + let link = ctx.link().clone(); + let name = image_file.name().clone(); + let file_type = image_file.raw_mime_type(); + let task = { + gloo::file::callbacks::read_as_bytes(&image_file, move |res| { + link.send_message(Msg::Loaded(FileDetails{ + name, + file_type, + data: res.expect("failed to read file"), + })); + }) + }; + self.readers.insert(image_file.name(), task); + + } + } + true + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
+
+ + + +
+
+ { for self.files.iter().map(Self::view_file) } +
+
+ } + } +} + +impl App { + fn view_file(file: &FileDetails) -> Html { + let src = format!( + "data:{};base64,{}", + file.file_type, + STANDARD.encode(&file.data) + ); + html! { +
+

{ format!("{}", file.name) }

+
+ +
+
+ } + } +} + +fn main() { + yew::Renderer::::new().render(); +} \ No newline at end of file From e668cc58ef2744312d36241b43a527d4a01008a4 Mon Sep 17 00:00:00 2001 From: Kenzi Connor Date: Fri, 19 Jul 2024 23:29:54 -0700 Subject: [PATCH 2/3] add field --- examples/form_file/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/form_file/src/main.rs b/examples/form_file/src/main.rs index 02b79558b0d..7571ff56de8 100644 --- a/examples/form_file/src/main.rs +++ b/examples/form_file/src/main.rs @@ -10,6 +10,7 @@ pub struct FileDetails { name: String, file_type: String, data: Vec, + alt_text: String, } pub enum Msg { @@ -46,6 +47,8 @@ impl Component for App { let form: HtmlFormElement = event.target_unchecked_into(); let form_data = FormData::new_with_form(&form).expect("form data"); let image_file = File::from(RawFile::from(form_data.get("file"))); + let alt_text = form_data.get("alt-text").as_string().unwrap(); + let link = ctx.link().clone(); let name = image_file.name().clone(); let file_type = image_file.raw_mime_type(); @@ -53,6 +56,7 @@ impl Component for App { gloo::file::callbacks::read_as_bytes(&image_file, move |res| { link.send_message(Msg::Loaded(FileDetails{ name, + alt_text, file_type, data: res.expect("failed to read file"), })); @@ -69,7 +73,8 @@ impl Component for App { html! {
- + {"Alt Text"} +

{ format!("{}", file.name) }

- + {file.alt_text.clone()}/
} From c854ca612c6db9a7297bc3b7ba5c971282ea76bd Mon Sep 17 00:00:00 2001 From: Kenzi Connor Date: Sat, 20 Jul 2024 00:14:01 -0700 Subject: [PATCH 3/3] file processing async starts upon file selection form submit button disabled till it's done. A more thorough check wouldn't let the form submit if there are readers left --- Cargo.lock | 99 ++++++---------------------------- examples/form_file/README.md | 11 +++- examples/form_file/src/main.rs | 86 +++++++++++++++++------------ 3 files changed, 77 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2075b835f7..4fb288549fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,6 +867,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_file" +version = "0.1.0" +dependencies = [ + "base64 0.21.5", + "gloo 0.10.0", + "gloo-console 0.3.0", + "js-sys", + "web-sys", + "yew", +] + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -2112,16 +2124,6 @@ dependencies = [ "yew", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -2207,12 +2209,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "papergrid" version = "0.10.0" @@ -2787,15 +2783,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2812,14 +2799,14 @@ dependencies = [ "bytes", "clap", "futures 0.3.29", + "log", "reqwest", "serde", - "time", "tokio", - "tracing-subscriber", - "tracing-web", "uuid", "warp", + "wasm-bindgen-futures", + "wasm-logger", "yew", ] @@ -3058,16 +3045,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "time" version = "0.3.30" @@ -3075,7 +3052,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", - "js-sys", "powerfmt", "serde", "time-core", @@ -3310,45 +3286,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tracing-web" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" -dependencies = [ - "js-sys", - "tracing-core", - "tracing-subscriber", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -3485,12 +3422,6 @@ dependencies = [ "serde", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/examples/form_file/README.md b/examples/form_file/README.md index 55b0e4dedf0..45a99e0b22d 100644 --- a/examples/form_file/README.md +++ b/examples/form_file/README.md @@ -1,12 +1,19 @@ # Form /w File Upload Example -[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fform_file)](https://examples.yew.rs/vform_file) +[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fform_file)](https://examples.yew.rs/form_file) -This example shows managing a file_data and related form fields together +This example shows some more comlicated interactions between file uploads, forms, and node_refs. + +The file selector change disables the form button untill the file is done being processed, at which point it's stashed in App state untill the form is submitted and it's stored in the FileDetails. ## Concepts Demonstrates reading from files in Yew with the help of [`gloo::file`](https://docs.rs/gloo-file/latest/gloo_file/). +Check the file_upload example for the simpler case of just uploading a file. + +## Todo + + - [] disabled form entirely by checking if there are readers left before allowing submit to proceed ## Running diff --git a/examples/form_file/src/main.rs b/examples/form_file/src/main.rs index 7571ff56de8..48b028a8e44 100644 --- a/examples/form_file/src/main.rs +++ b/examples/form_file/src/main.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; -use gloo::file::{callbacks::FileReader, File}; +use gloo::file::{callbacks::FileReader, File, FileList}; use gloo_console::debug; -use web_sys::{FormData, HtmlFormElement, File as RawFile}; +use web_sys::{File as RawFile, FormData, HtmlFormElement, HtmlInputElement}; use yew::prelude::*; pub struct FileDetails { @@ -14,13 +14,16 @@ pub struct FileDetails { } pub enum Msg { - Loaded(FileDetails), + Loaded(String, Vec), Submit(SubmitEvent), + File(FileList), } pub struct App { readers: HashMap, files: Vec, + button: NodeRef, + file_data: Vec, } impl Component for App { @@ -31,39 +34,52 @@ impl Component for App { Self { readers: HashMap::default(), files: Vec::default(), + button: NodeRef::default(), + file_data: Vec::default(), } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Msg::Loaded(file) => { - let name = file.name.clone(); - self.files.push(file); + Msg::Loaded(name, data) => { + let submit = self.button.cast::().expect("button"); + self.file_data = data; + submit.set_disabled(false); self.readers.remove(&name); } + Msg::File(files) => { + let submit = self.button.cast::().expect("button"); + submit.set_disabled(true); + + let file = files[0].clone(); + let link = ctx.link().clone(); + let name = file.name().clone(); + let task = { + gloo::file::callbacks::read_as_bytes(&file, move |res| { + link.send_message(Msg::Loaded(name, res.expect("failed to read file"))); + }) + }; + self.readers.insert(file.name(), task); + } Msg::Submit(event) => { - debug!(event.clone()); - event.prevent_default(); + debug!(event.clone()); + event.prevent_default(); let form: HtmlFormElement = event.target_unchecked_into(); let form_data = FormData::new_with_form(&form).expect("form data"); let image_file = File::from(RawFile::from(form_data.get("file"))); + let alt_text = form_data.get("alt-text").as_string().unwrap(); + let name = image_file.name(); + let data = self.file_data.clone(); - let link = ctx.link().clone(); - let name = image_file.name().clone(); let file_type = image_file.raw_mime_type(); - let task = { - gloo::file::callbacks::read_as_bytes(&image_file, move |res| { - link.send_message(Msg::Loaded(FileDetails{ - name, - alt_text, - file_type, - data: res.expect("failed to read file"), - })); - }) - }; - self.readers.insert(image_file.name(), task); - + self.files.push(FileDetails { + alt_text, + name, + data, + file_type, + }); + self.file_data = Vec::default(); } } true @@ -73,16 +89,20 @@ impl Component for App { html! {
- {"Alt Text"} - - - + {"Alt Text"} + + +
{ for self.files.iter().map(Self::view_file) } @@ -112,4 +132,4 @@ impl App { fn main() { yew::Renderer::::new().render(); -} \ No newline at end of file +}