├── yew_form ├── src │ ├── components │ │ ├── mod.rs │ │ ├── checkbox.rs │ │ ├── file.rs │ │ ├── select.rs │ │ ├── textarea.rs │ │ └── field.rs │ ├── form_field.rs │ ├── lib.rs │ ├── form_state_serde_error.rs │ ├── model.rs │ ├── form.rs │ ├── field.rs │ ├── form_state.rs │ ├── form_state_deserializer.rs │ └── form_state_serializer.rs └── Cargo.toml ├── examples └── form │ ├── README.md │ ├── Cargo.toml │ ├── static │ └── index.html │ ├── src │ └── lib.rs │ ├── Cargo.lock │ └── test.rs ├── .gitignore ├── yew_form_derive ├── Cargo.toml ├── src │ └── lib.rs └── Cargo.lock ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /yew_form/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod checkbox; 2 | pub mod field; 3 | pub mod file; 4 | pub mod select; 5 | pub mod textarea; 6 | 7 | pub use checkbox::CheckBox; 8 | pub use file::File; 9 | pub use select::Select; 10 | pub use textarea::TextArea; 11 | -------------------------------------------------------------------------------- /examples/form/README.md: -------------------------------------------------------------------------------- 1 | # Simple form example 2 | 3 | To run: 4 | 5 | Install the following if desired: 6 | ```bash 7 | cargo install wasm-pack 8 | cargo install miniserve 9 | ``` 10 | 11 | Compile and run: 12 | ```bash 13 | wasm-pack build --target web --out-name wasm --out-dir ./static 14 | miniserve ./static --index index.html 15 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | yew_form/Cargo.lock 8 | yew_form_derive/Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | /.idea/ 14 | -------------------------------------------------------------------------------- /examples/form/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form" 3 | version = "0.1.8" 4 | authors = ["J-F Bilodeau "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | lazy_static = "1.4" 12 | regex = "1" 13 | validator = "0.14" 14 | validator_derive = "0.14" 15 | wasm-bindgen = "0.2" 16 | yew = "0.19.3" 17 | yew_form = { version = "0.1", path = "../../yew_form"} 18 | yew_form_derive = { version = "0.1", path = "../../yew_form_derive"} 19 | -------------------------------------------------------------------------------- /yew_form/src/form_field.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct FormField { 2 | pub field_name: String, 3 | pub field_value: String, 4 | pub message: String, 5 | pub dirty: bool, 6 | pub valid: bool, 7 | } 8 | 9 | impl FormField { 10 | pub fn new(field_name: &str, field_value: &str) -> Self { 11 | FormField { 12 | field_name: String::from(field_name), 13 | field_value: String::from(field_value), 14 | message: String::new(), 15 | dirty: false, 16 | valid: true, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /yew_form/src/lib.rs: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | pub use checkbox::CheckBox; 3 | pub use field::Field; 4 | pub use form::Form; 5 | pub use model::{split_field_path, Model}; 6 | 7 | pub mod checkbox; 8 | pub mod field; 9 | ======= 10 | pub mod components; 11 | >>>>>>> 4b9fabffb63393ec7626a4477fd36de12a07fac9 12 | pub mod form; 13 | pub mod form_field; 14 | pub mod form_state; 15 | pub mod model; 16 | 17 | pub use components::checkbox::CheckBox; 18 | pub use components::field::Field; 19 | pub use components::file::File; 20 | pub use components::select::Select; 21 | pub use components::textarea::TextArea; 22 | pub use form::Form; 23 | pub use model::{split_field_path, Model}; 24 | -------------------------------------------------------------------------------- /yew_form_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew_form_derive" 3 | version = "0.1.8" 4 | authors = ["J-F Bilodeau "] 5 | edition = "2018" 6 | description = "Bringing MVC to Yew! A set mildly opinionated of Yew component to map and validate a model to a HTML form" 7 | license = "MIT" 8 | repository = "https://github.com/jfbilodeau/yew_form" 9 | readme = "../README.md" 10 | categories = [ "web-programming" ] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | proc-macro2 = "1.0.28" 19 | quote = "1.0.9" 20 | syn = { version = "1", features = ["full"] } 21 | yew_form = { version = "0.1", path = "../yew_form" } -------------------------------------------------------------------------------- /yew_form/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew_form" 3 | version = "0.1.8" 4 | authors = ["J-F Bilodeau "] 5 | edition = "2018" 6 | description = "Bringing MVC to Yew! A set mildly opinionated Yew component to map and validate a model to a HTML form" 7 | license = "MIT" 8 | repository = "https://github.com/thebearjew/yew_form" 9 | readme = "../README.md" 10 | categories = [ "web-programming" ] 11 | 12 | [dependencies] 13 | <<<<<<< HEAD 14 | validator = "0.14.0" 15 | validator_derive = "0.14.0" 16 | yew = "0.18" 17 | ======= 18 | wasm-bindgen = "0.2" 19 | validator = "0.14.0" 20 | validator_derive = "0.14.0" 21 | yew = "0.19.3" 22 | gloo-console = "0.2" 23 | 24 | [dependencies.web-sys] 25 | version = "0.3" 26 | features = [ 27 | "HtmlInputElement", 28 | "HtmlSelectElement", 29 | "HtmlTextAreaElement" 30 | ] 31 | >>>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 32 | -------------------------------------------------------------------------------- /yew_form/src/form_state_serde_error.rs: -------------------------------------------------------------------------------- 1 | use std::string::ToString; 2 | use std::fmt::Display; 3 | use std::error::Error; 4 | 5 | #[derive(Clone, Debug, PartialEq)] 6 | pub enum ModelSerializerError { 7 | Message(String), 8 | } 9 | 10 | impl serde::ser::Error for ModelSerializerError { 11 | fn custom(msg: T) -> Self { 12 | ModelSerializerError::Message(msg.to_string()) 13 | } 14 | } 15 | 16 | impl serde::de::Error for ModelSerializerError { 17 | fn custom(msg: T) -> Self { 18 | ModelSerializerError::Message(msg.to_string()) 19 | } 20 | } 21 | 22 | 23 | impl Display for ModelSerializerError { 24 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 25 | formatter.write_str(std::error::Error::description(self)) 26 | } 27 | } 28 | 29 | impl std::error::Error for ModelSerializerError { 30 | fn description(&self) -> &str { 31 | match *self { 32 | ModelSerializerError::Message(ref msg) => msg, 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/form/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form Example 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Loading...

13 |
14 | Loading... 15 |
16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /yew_form/src/model.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use validator::Validate; 3 | 4 | pub trait FormValue { 5 | fn fields(&self, prefix: &str, fields: &mut Vec) { 6 | // By default, announce the value to be a scalar 7 | fields.push(String::from(prefix)); 8 | } 9 | fn value(&self, field_path: &str) -> String; 10 | fn set_value(&mut self, field_path: &str, value: &str) -> Result<(), String>; 11 | } 12 | 13 | pub trait Model: FormValue + Validate + PartialEq + Clone + 'static {} 14 | 15 | pub fn split_field_path(field_path: &str) -> (&str, &str) { 16 | if let Some(index) = field_path.find(".") { 17 | (&field_path[0..index], &field_path[index + 1..]) 18 | } else { 19 | (field_path, "") 20 | } 21 | } 22 | 23 | impl FormValue for T { 24 | fn value(&self, field_path: &str) -> String { 25 | debug_assert!(field_path == ""); 26 | 27 | self.to_string() 28 | } 29 | 30 | fn set_value(&mut self, field_path: &str, value: &str) -> Result<(), String> { 31 | debug_assert!(field_path == ""); 32 | 33 | if let Ok(v) = value.parse::() { 34 | *self = v; 35 | Ok(()) 36 | } else { 37 | Err(String::from("Could not convert")) 38 | } 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::model::split_field_path; 45 | 46 | #[test] 47 | fn test_split_field_path() { 48 | let path = "field"; 49 | let (field, suffix) = split_field_path(path); 50 | 51 | assert_eq!(field, "field"); 52 | assert_eq!(suffix, ""); 53 | 54 | let path = "field.sub"; 55 | let (field, suffix) = split_field_path(path); 56 | 57 | assert_eq!(field, "field"); 58 | assert_eq!(suffix, "sub"); 59 | 60 | let path = "field.sub.subsub"; 61 | let (field, suffix) = split_field_path(path); 62 | 63 | assert_eq!(field, "field"); 64 | assert_eq!(suffix, "sub.subsub"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /yew_form/src/components/checkbox.rs: -------------------------------------------------------------------------------- 1 | use crate::{Form, Model}; 2 | <<<<<<< HEAD 3 | use yew::{html, Callback, Component, ComponentLink, Html, Properties}; 4 | ======= 5 | use yew::{html, Callback, Component, Context, Html, Properties}; 6 | >>>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 7 | 8 | pub enum CheckBoxMessage { 9 | OnToggle, 10 | } 11 | 12 | #[derive(Properties, PartialEq, Clone)] 13 | pub struct CheckBoxProperties { 14 | pub field_name: String, 15 | pub form: Form, 16 | #[prop_or_else(Callback::noop)] 17 | pub ontoggle: Callback, 18 | } 19 | 20 | pub struct CheckBox { 21 | props: CheckBoxProperties, 22 | } 23 | 24 | impl CheckBox { 25 | fn value(&self) -> bool { 26 | let field_path = &self.props.field_name; 27 | 28 | self.props.form.field_value(field_path) == "true" 29 | } 30 | 31 | fn set_value(&mut self, value: bool) { 32 | let field_path = &self.props.field_name; 33 | 34 | self.props 35 | .form 36 | .set_field_value(field_path, &value.to_string()); 37 | } 38 | } 39 | 40 | impl Component for CheckBox { 41 | type Message = CheckBoxMessage; 42 | type Properties = CheckBoxProperties; 43 | 44 | <<<<<<< HEAD 45 | fn create(props: Self::Properties, link: ComponentLink) -> Self { 46 | Self { link, props } 47 | ======= 48 | fn create(ctx: &yew::Context) -> Self { 49 | Self { 50 | props: ctx.props().clone(), 51 | } 52 | >>>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 53 | } 54 | 55 | fn update(&mut self, ctx: &yew::Context, msg: Self::Message) -> bool { 56 | match msg { 57 | CheckBoxMessage::OnToggle => { 58 | let value = !self.value(); 59 | self.set_value(value); 60 | ctx.props().ontoggle.emit(value); 61 | true 62 | } 63 | } 64 | } 65 | 66 | fn changed(&mut self, _ctx: &yew::Context) -> bool { 67 | true 68 | } 69 | 70 | fn view(&self, ctx: &Context) -> Html { 71 | html! { 72 | >>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 83 | class="form-check-input form-input" 84 | /> 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /yew_form/src/form.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefCell, RefMut}; 2 | use std::rc::Rc; 3 | <<<<<<< HEAD 4 | ======= 5 | use yew::html::ImplicitClone; 6 | >>>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 7 | 8 | use crate::form_state::FormState; 9 | use crate::Model; 10 | 11 | #[derive(Clone)] 12 | pub struct Form { 13 | state: Rc>>, 14 | } 15 | 16 | impl ImplicitClone for Form {} 17 | 18 | impl Form { 19 | pub fn new(model: T) -> Form { 20 | Form { 21 | state: Rc::new(RefCell::new(FormState::new(model))), 22 | } 23 | } 24 | 25 | pub(crate) fn state(&self) -> Ref> { 26 | self.state.as_ref().borrow() 27 | } 28 | 29 | pub(crate) fn state_mut(&mut self) -> RefMut> { 30 | self.state.borrow_mut() 31 | } 32 | 33 | pub fn validate(&mut self) -> bool { 34 | self.state_mut().validate() 35 | } 36 | 37 | pub fn valid(&self) -> bool { 38 | self.state().valid() 39 | } 40 | 41 | pub fn field_value(&self, field_name: &str) -> String { 42 | self.state().field(field_name).field_value.to_owned() 43 | } 44 | 45 | pub fn field_valid(&self, field_name: &str) -> bool { 46 | self.state().field_valid(field_name) 47 | } 48 | 49 | pub fn field_message(&self, field_name: &str) -> String { 50 | self.state().field(field_name).message.to_owned() 51 | } 52 | 53 | pub fn set_field_value(&mut self, field_name: &str, field_value: &str) { 54 | self.state_mut().set_field_value(field_name, field_value); 55 | } 56 | 57 | /// returns a clone of the inner model 58 | pub fn model(&self) -> T { 59 | self.state().model().clone() 60 | } 61 | } 62 | 63 | impl PartialEq for Form { 64 | fn eq(&self, other: &Self) -> bool { 65 | <<<<<<< HEAD 66 | Rc::ptr_eq(&self.state, &other.state) 67 | || self.state.borrow().model == other.state.borrow().model 68 | ======= 69 | Rc::ptr_eq(&self.state, &other.state) || self.state().model == other.state().model 70 | >>>>>>> fa67514a4897880b89e3e13161797e6877d3f50b 71 | } 72 | 73 | fn ne(&self, other: &Self) -> bool { 74 | self.state().model != other.state().model 75 | } 76 | } 77 | 78 | // impl Component for Form { 79 | // type Message = (); 80 | // type Properties = FormProperties; 81 | // 82 | // fn create(props: Self::Properties, link: ComponentLink) -> Self { 83 | // Self { 84 | // children: props.children, 85 | // state: T::new(), 86 | // } 87 | // } 88 | // 89 | // fn update(&mut self, msg: Self::Message) -> bool { 90 | // unimplemented!() 91 | // } 92 | // 93 | // fn view(&self) -> Html { 94 | // unimplemented!() 95 | // } 96 | // } 97 | -------------------------------------------------------------------------------- /yew_form_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "65536"] 2 | #[macro_use] 3 | extern crate quote; 4 | extern crate syn; 5 | 6 | use proc_macro::TokenStream; 7 | use quote::ToTokens; 8 | 9 | #[proc_macro_derive(Model)] 10 | pub fn derive_model(input: TokenStream) -> TokenStream { 11 | let ast: syn::DeriveInput = syn::parse(input).unwrap(); 12 | 13 | let fields: Vec = match ast.data { 14 | syn::Data::Struct(syn::DataStruct { ref fields, .. }) => { 15 | if fields.iter().any(|field| field.ident.is_none()) { 16 | panic!("#[derive(Model)] struct cannot have unnamed field"); 17 | } 18 | fields.iter().cloned().collect() 19 | } 20 | _ => panic!("#[derive(Model)] can only be used with structs"), 21 | }; 22 | 23 | let mut field_idents: Vec = vec![]; 24 | let mut field_names: Vec = vec![]; 25 | let mut field_types: Vec = vec![]; 26 | 27 | for field in &fields { 28 | let field_ident = field.ident.clone().unwrap(); 29 | let field_name = field_ident.to_string(); 30 | let field_type = match field.ty { 31 | syn::Type::Path(syn::TypePath { ref path, .. }) => { 32 | let mut tokens = proc_macro2::TokenStream::new(); 33 | path.to_tokens(&mut tokens); 34 | tokens.to_string().replace(' ', "") 35 | } 36 | _ => panic!( 37 | "Type `{:?}` of field `{}` is not supported", 38 | field.ty, field_ident 39 | ), 40 | }; 41 | 42 | field_idents.push(field_ident); 43 | field_names.push(field_name); 44 | field_types.push(field_type); 45 | } 46 | 47 | let struct_name = &ast.ident; 48 | let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); 49 | 50 | let impl_ast = quote! { 51 | impl #impl_generics ::yew_form::model::Model for #struct_name #ty_generics #where_clause { 52 | } 53 | 54 | impl #impl_generics ::yew_form::model::FormValue for #struct_name #ty_generics #where_clause { 55 | fn fields(&self, prefix: &str, fields: &mut Vec) { 56 | let field_prefix = if prefix == "" { 57 | String::new() 58 | } else { 59 | format!("{}.", prefix) 60 | }; 61 | 62 | #( 63 | let field_path = format!("{}{}", field_prefix, #field_names); 64 | self.#field_idents.fields(&field_path, fields); 65 | )* 66 | } 67 | 68 | fn value(&self, field_path: &str) -> String { 69 | let (field_name, suffix) = ::yew_form::split_field_path(field_path); 70 | 71 | match field_name { 72 | #( 73 | #field_names => self.#field_idents.value(suffix), 74 | )* 75 | _ => panic!("Field {} does not exist in {}", field_path, stringify!(#struct_name)) 76 | } 77 | } 78 | 79 | fn set_value(&mut self, field_path: &str, value: &str) -> Result<(), String> { 80 | let (field_name, suffix) = ::yew_form::split_field_path(field_path); 81 | 82 | match field_name { 83 | #( 84 | #field_names => self.#field_idents.set_value(suffix, value), 85 | )* 86 | _ => panic!("Field {} does not exist in {}", field_path, stringify!(#struct_name)) 87 | } 88 | } 89 | } 90 | }; 91 | 92 | impl_ast.into() 93 | } 94 | -------------------------------------------------------------------------------- /yew_form/src/field.rs: -------------------------------------------------------------------------------- 1 | use yew::{Component, ComponentLink, Html, html, Properties, InputData, Callback, ShouldRender}; 2 | 3 | use crate::form::{Form}; 4 | use crate::{Model}; 5 | 6 | pub enum FieldMessage { 7 | OnInput(InputData) 8 | } 9 | 10 | fn default_text() -> String { 11 | String::from("text") 12 | } 13 | 14 | #[derive(Properties, PartialEq, Clone)] 15 | pub struct FieldProperties { 16 | #[prop_or_else(default_text)] 17 | pub input_type: String, 18 | pub field_name: String, 19 | pub form: Form, 20 | #[prop_or_else(String::new)] 21 | pub placeholder: String, 22 | #[prop_or_else(|| { "form-control".to_owned() })] 23 | pub class: String, 24 | #[prop_or_else(|| { "is-invalid".to_owned() })] 25 | pub class_invalid: String, 26 | #[prop_or_else(|| { "is-valid".to_owned() })] 27 | pub class_valid: String, 28 | #[prop_or_else(Callback::noop)] 29 | pub oninput: Callback, 30 | } 31 | 32 | pub struct Field { 33 | link: ComponentLink, 34 | pub input_type: String, 35 | pub field_name: String, 36 | pub form: Form, 37 | pub placeholder: String, 38 | pub class: String, 39 | pub class_invalid: String, 40 | pub class_valid: String, 41 | pub oninput: Callback, 42 | } 43 | 44 | impl Field { 45 | pub fn field_name(&self) -> &str { 46 | &self.field_name 47 | } 48 | 49 | pub fn class(&self) -> String { 50 | let s = self.form.state(); 51 | let field = s.field(&self.field_name); 52 | 53 | if field.dirty && field.valid { 54 | format!("{} {}", self.class, self.class_valid) 55 | } else if field.dirty { 56 | format!("{} {}", self.class, self.class_invalid) 57 | } else { 58 | self.class.to_owned() 59 | } 60 | } 61 | 62 | 63 | pub fn message(&self) -> String { 64 | self.form.field_message(&self.field_name()) 65 | } 66 | 67 | pub fn valid(&self) -> bool { 68 | self.form.field_valid(&self.field_name()) 69 | } 70 | 71 | pub fn dirty(&self) -> bool { 72 | self.form.state().field(&self.field_name).dirty 73 | } 74 | 75 | pub fn set_field(&mut self, field_name: &str, value: &str) { 76 | self.form.set_field_value(field_name, value) 77 | } 78 | } 79 | 80 | impl Component for Field { 81 | type Message = FieldMessage; 82 | type Properties = FieldProperties; 83 | 84 | fn create(props: Self::Properties, link: ComponentLink) -> Self { 85 | let mut form_field = Self { 86 | link, 87 | input_type: String::from(props.input_type), 88 | field_name: String::from(props.field_name), 89 | form: props.form, 90 | placeholder: String::from(props.placeholder), 91 | oninput: props.oninput, 92 | class: props.class, 93 | class_invalid: props.class_invalid, 94 | class_valid: props.class_valid, 95 | }; 96 | 97 | if form_field.input_type == "" { 98 | form_field.input_type = String::from("text"); 99 | } 100 | 101 | form_field 102 | } 103 | 104 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 105 | match msg { 106 | FieldMessage::OnInput(input_data) => { 107 | let mut state = self.form.state_mut(); 108 | state.set_field_value(&self.field_name, &input_data.value); 109 | state.update_validation_field(&self.field_name); 110 | drop(state); 111 | 112 | self.oninput.emit(input_data); 113 | true 114 | } 115 | } 116 | } 117 | 118 | fn change(&mut self, _props: Self::Properties) -> ShouldRender { 119 | true 120 | } 121 | 122 | fn view(&self) -> Html { 123 | html! { 124 | 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /yew_form/src/components/file.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | use wasm_bindgen::UnwrapThrowExt; 3 | use web_sys::Event; 4 | use web_sys::HtmlInputElement; 5 | use web_sys::InputEvent; 6 | use yew::{classes, html, Callback, Classes, Component, Context, Html, Properties}; 7 | 8 | use crate::form::Form; 9 | use crate::Model; 10 | 11 | pub enum FileMessage { 12 | OnInput(InputEvent), 13 | } 14 | 15 | #[derive(Properties, PartialEq, Clone)] 16 | pub struct FilePropeties { 17 | pub form: Form, 18 | pub field_name: String, 19 | #[prop_or_else(|| false )] 20 | pub disabled: bool, 21 | #[prop_or_else(|| false )] 22 | pub multiple: bool, 23 | #[prop_or_default] 24 | pub accept: String, 25 | #[prop_or_default] 26 | pub capture: String, 27 | #[prop_or_default] 28 | pub class: Classes, 29 | #[prop_or_default] 30 | pub class_valid: Classes, 31 | #[prop_or_default] 32 | pub class_invalid: Classes, 33 | #[prop_or_else(Callback::noop)] 34 | pub oninput: Callback, 35 | } 36 | 37 | pub struct File { 38 | pub form: Form, 39 | pub field_name: String, 40 | pub class: Classes, 41 | pub class_valid: Classes, 42 | pub class_invalid: Classes, 43 | } 44 | 45 | impl File { 46 | pub fn field_name(&self) -> &str { 47 | &self.field_name 48 | } 49 | 50 | pub fn class(&self) -> Classes { 51 | let s = self.form.state(); 52 | let field = s.field(&self.field_name); 53 | 54 | if field.dirty && field.valid { 55 | classes!(self.class.clone(), self.class_valid.clone()) 56 | } else if field.dirty { 57 | classes!(self.class.clone(), self.class_invalid.clone()) 58 | } else { 59 | self.class.to_owned() 60 | } 61 | } 62 | 63 | pub fn message(&self) -> String { 64 | self.form.field_message(self.field_name()) 65 | } 66 | 67 | pub fn valid(&self) -> bool { 68 | self.form.field_valid(self.field_name()) 69 | } 70 | 71 | pub fn dirty(&self) -> bool { 72 | self.form.state().field(self.field_name()).dirty 73 | } 74 | 75 | pub fn set_field(&mut self, field_name: &str, value: &str) { 76 | self.form.set_field_value(field_name, value) 77 | } 78 | 79 | pub fn get_select_value(&self, e: InputEvent) -> String { 80 | let event: Event = e.dyn_into().unwrap_throw(); 81 | let event_target = event.target().unwrap_throw(); 82 | let target: HtmlInputElement = event_target.dyn_into().unwrap_throw(); 83 | target.value() 84 | } 85 | } 86 | 87 | impl Component for File { 88 | type Message = FileMessage; 89 | type Properties = FilePropeties; 90 | 91 | fn create(ctx: &Context) -> Self { 92 | Self { 93 | form: ctx.props().form.clone(), 94 | field_name: String::from(&ctx.props().field_name), 95 | class: ctx.props().class.clone(), 96 | class_valid: ctx.props().class_valid.clone(), 97 | class_invalid: ctx.props().class_invalid.clone(), 98 | } 99 | } 100 | 101 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 102 | match msg { 103 | FileMessage::OnInput(event) => { 104 | let value = self.get_select_value(event.clone()); 105 | let mut state = self.form.state_mut(); 106 | state.set_field_value(&self.field_name, &value); 107 | state.update_validation_field(&self.field_name); 108 | drop(state); 109 | ctx.props().oninput.emit(event); 110 | true 111 | } 112 | } 113 | } 114 | 115 | fn changed(&mut self, _ctx: &Context) -> bool { 116 | true 117 | } 118 | 119 | fn view(&self, ctx: &Context) -> Html { 120 | let props = ctx.props(); 121 | 122 | html! { 123 | 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /yew_form/src/components/select.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | use wasm_bindgen::UnwrapThrowExt; 3 | use web_sys::Event; 4 | use web_sys::HtmlSelectElement; 5 | use web_sys::InputEvent; 6 | use yew::{classes, html, Callback, Children, Classes, Component, Context, Html, Properties}; 7 | 8 | use crate::form::Form; 9 | use crate::Model; 10 | 11 | pub enum SelectMessage { 12 | OnInput(InputEvent), 13 | } 14 | 15 | #[derive(Properties, PartialEq, Clone)] 16 | pub struct SelectPropeties { 17 | pub form: Form, 18 | pub field_name: String, 19 | #[prop_or_else(|| "off".to_owned() )] 20 | pub autocomplete: String, 21 | #[prop_or_else(|| false )] 22 | pub disabled: bool, 23 | #[prop_or_else(|| false )] 24 | pub multiple: bool, 25 | #[prop_or_default] 26 | pub class: Classes, 27 | #[prop_or_default] 28 | pub class_valid: Classes, 29 | #[prop_or_default] 30 | pub class_invalid: Classes, 31 | pub children: Children, 32 | #[prop_or_else(Callback::noop)] 33 | pub oninput: Callback, 34 | } 35 | 36 | pub struct Select { 37 | pub form: Form, 38 | pub field_name: String, 39 | pub class: Classes, 40 | pub class_valid: Classes, 41 | pub class_invalid: Classes, 42 | } 43 | 44 | impl Select { 45 | pub fn field_name(&self) -> &str { 46 | &self.field_name 47 | } 48 | 49 | pub fn class(&self) -> Classes { 50 | let s = self.form.state(); 51 | let field = s.field(&self.field_name); 52 | 53 | if field.dirty && field.valid { 54 | classes!(self.class.clone(), self.class_valid.clone()) 55 | } else if field.dirty { 56 | classes!(self.class.clone(), self.class_invalid.clone()) 57 | } else { 58 | self.class.to_owned() 59 | } 60 | } 61 | 62 | pub fn message(&self) -> String { 63 | self.form.field_message(self.field_name()) 64 | } 65 | 66 | pub fn valid(&self) -> bool { 67 | self.form.field_valid(self.field_name()) 68 | } 69 | 70 | pub fn dirty(&self) -> bool { 71 | self.form.state().field(self.field_name()).dirty 72 | } 73 | 74 | pub fn set_field(&mut self, field_name: &str, value: &str) { 75 | self.form.set_field_value(field_name, value) 76 | } 77 | 78 | pub fn get_select_value(&self, e: InputEvent) -> String { 79 | let event: Event = e.dyn_into().unwrap_throw(); 80 | let event_target = event.target().unwrap_throw(); 81 | let target: HtmlSelectElement = event_target.dyn_into().unwrap_throw(); 82 | target.value() 83 | } 84 | } 85 | 86 | impl Component for Select { 87 | type Message = SelectMessage; 88 | type Properties = SelectPropeties; 89 | 90 | fn create(ctx: &Context) -> Self { 91 | Self { 92 | form: ctx.props().form.clone(), 93 | field_name: String::from(&ctx.props().field_name), 94 | class: ctx.props().class.clone(), 95 | class_valid: ctx.props().class_valid.clone(), 96 | class_invalid: ctx.props().class_invalid.clone(), 97 | } 98 | } 99 | 100 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 101 | match msg { 102 | SelectMessage::OnInput(event) => { 103 | let value = self.get_select_value(event.clone()); 104 | let mut state = self.form.state_mut(); 105 | state.set_field_value(&self.field_name, &value); 106 | state.update_validation_field(&self.field_name); 107 | drop(state); 108 | ctx.props().oninput.emit(event); 109 | true 110 | } 111 | } 112 | } 113 | 114 | fn changed(&mut self, _ctx: &Context) -> bool { 115 | true 116 | } 117 | 118 | fn view(&self, ctx: &Context) -> Html { 119 | let props = ctx.props(); 120 | 121 | html! { 122 | 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /yew_form/src/components/textarea.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | use wasm_bindgen::UnwrapThrowExt; 3 | use web_sys::Event; 4 | use web_sys::HtmlTextAreaElement; 5 | use web_sys::InputEvent; 6 | 7 | use yew::{classes, html, Callback, Classes, Component, Context, Html, Properties}; 8 | 9 | use crate::form::Form; 10 | use crate::Model; 11 | 12 | pub enum TextAreaMessage { 13 | OnInput(InputEvent), 14 | } 15 | 16 | #[derive(Properties, PartialEq, Clone)] 17 | pub struct TextAreaProperties { 18 | pub form: Form, 19 | pub field_name: String, 20 | pub oninput: Callback, 21 | #[prop_or_default] 22 | pub class: Classes, 23 | #[prop_or_default] 24 | pub class_invalid: Classes, 25 | #[prop_or_default] 26 | pub class_valid: Classes, 27 | #[prop_or(String::from("20"))] 28 | pub cols: String, 29 | #[prop_or(String::from("5"))] 30 | pub rows: String, 31 | #[prop_or_default] 32 | pub placeholder: String, 33 | #[prop_or_default] 34 | pub disabled: bool, 35 | #[prop_or(String::from("soft"))] 36 | pub wrap: String, 37 | #[prop_or_default] 38 | pub spellcheck: bool, 39 | #[prop_or(String::from(""))] 40 | pub autocomplete: String, 41 | #[prop_or(String::from(""))] 42 | pub autocorrect: String, 43 | } 44 | 45 | pub struct TextArea { 46 | pub form: Form, 47 | pub field_name: String, 48 | pub class: Classes, 49 | pub class_valid: Classes, 50 | pub class_invalid: Classes, 51 | } 52 | 53 | impl TextArea { 54 | pub fn field_name(&self) -> &str { 55 | &self.field_name 56 | } 57 | 58 | pub fn class(&self) -> Classes { 59 | let s = self.form.state(); 60 | let field = s.field(&self.field_name); 61 | 62 | if field.dirty && field.valid { 63 | classes!(self.class.clone(), self.class_valid.clone()) 64 | } else if field.dirty { 65 | classes!(self.class.clone(), self.class_invalid.clone()) 66 | } else { 67 | self.class.to_owned() 68 | } 69 | } 70 | 71 | pub fn message(&self) -> String { 72 | self.form.field_message(self.field_name()) 73 | } 74 | 75 | pub fn valid(&self) -> bool { 76 | self.form.field_valid(self.field_name()) 77 | } 78 | 79 | pub fn dirty(&self) -> bool { 80 | self.form.state().field(self.field_name()).dirty 81 | } 82 | 83 | pub fn set_field(&mut self, field_name: &str, value: &str) { 84 | self.form.set_field_value(field_name, value) 85 | } 86 | 87 | pub fn get_select_value(&self, e: InputEvent) -> String { 88 | let event: Event = e.dyn_into().unwrap_throw(); 89 | let event_target = event.target().unwrap_throw(); 90 | let target: HtmlTextAreaElement = event_target.dyn_into().unwrap_throw(); 91 | target.value() 92 | } 93 | } 94 | 95 | impl Component for TextArea { 96 | type Message = TextAreaMessage; 97 | type Properties = TextAreaProperties; 98 | 99 | fn create(ctx: &Context) -> Self { 100 | Self { 101 | form: ctx.props().form.clone(), 102 | field_name: String::from(&ctx.props().field_name), 103 | class: ctx.props().class.clone(), 104 | class_valid: ctx.props().class_valid.clone(), 105 | class_invalid: ctx.props().class_invalid.clone(), 106 | } 107 | } 108 | 109 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 110 | match msg { 111 | TextAreaMessage::OnInput(event) => { 112 | let value = self.get_select_value(event.clone()); 113 | let mut state = self.form.state_mut(); 114 | state.set_field_value(&self.field_name, &value); 115 | state.update_validation_field(&self.field_name); 116 | drop(state); 117 | ctx.props().oninput.emit(event); 118 | true 119 | } 120 | } 121 | } 122 | 123 | fn changed(&mut self, _ctx: &Context) -> bool { 124 | true 125 | } 126 | 127 | fn view(&self, ctx: &Context) -> Html { 128 | let props = ctx.props(); 129 | 130 | html! { 131 |