diff --git a/Cargo.toml b/Cargo.toml index 7a3cf17..5ca0b83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["storybook", "examples/form"] name = "vertigo-forms" version = "0.1.0" authors = ["MichaƂ Pokrywka "] -edition = "2021" +edition = "2024" [dependencies] base64 = "0.22" diff --git a/README.md b/README.md index 3dce120..5c9748e 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ Blocks for building forms in [vertigo](https://crates.io/crates/vertigo). [![CI](https://github.com/vertigo-web/vertigo-forms/actions/workflows/pipeline.yaml/badge.svg)](https://github.com/vertigo-web/vertigo-forms/actions/workflows/pipeline.yaml) [![downloads](https://img.shields.io/crates/d/vertigo-forms.svg)](https://crates.io/crates/vertigo-forms) -See [Changelog](https://github.com/vertigo-web/vertigo/blob/master/CHANGES.md) for recent features. +See [Changelog](https://github.com/vertigo-web/vertigo-forms/blob/master/CHANGES.md) for recent features. ## Example Dependencies: ```toml -vertigo = "0.7" +vertigo = "0.8" vertigo-forms = { git = "https://github.com/vertigo-web/vertigo-forms" } ``` diff --git a/src/drop_image_file.rs b/src/drop_image_file.rs index e0c1f34..156c64f 100644 --- a/src/drop_image_file.rs +++ b/src/drop_image_file.rs @@ -1,7 +1,7 @@ -use base64::{engine::general_purpose::STANDARD_NO_PAD as BASE_64, Engine as _}; +use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD as BASE_64}; use std::rc::Rc; use vertigo::{ - bind, computed_tuple, css, dom, Computed, Css, DomNode, DropFileEvent, DropFileItem, Value, + Computed, Css, DomNode, DropFileEvent, DropFileItem, Value, bind, computed_tuple, css, dom, }; /// Box that allows to accept image files on it, connected to `Value>`. @@ -13,7 +13,8 @@ pub struct DropImageFile { #[derive(Clone)] pub struct DropImageFileParams { - pub callback: Option>, + /// Custom callback when new image dropped, leave empty to automatically set/unset `item` + pub callback: Option)>>, pub revert_label: String, pub cancel_label: String, pub no_image_text: String, @@ -63,6 +64,7 @@ impl DropImageFile { ); let item_clone = self.item.clone(); let params = self.params.clone(); + let callback = self.params.callback.clone(); let image_view = view_deps.render_value(move |(original, item, base64_date)| match item { Some(item) => { let message = format_line(&item); @@ -70,7 +72,13 @@ impl DropImageFile { display: flex; flex-flow: column; "}; - let restore = bind!(item_clone, |_| item_clone.set(None)); + let restore = bind!(item_clone, callback, |_| { + if let Some(callback) = &callback { + callback(None); + } else { + item_clone.set(None); + } + }); let restore_text = if original.is_some() { ¶ms.revert_label } else { @@ -97,20 +105,14 @@ impl DropImageFile { let on_dropfile = move |event: DropFileEvent| { for file in event.items.into_iter() { if let Some(callback) = callback.as_deref() { - callback(file); + callback(Some(file)); } else { - item.change(|current| { - *current = Some(file); - }); + item.set(Some(file)); } } }; - let dropzone_css = self - .params - .dropzone_css - .clone() - .extend(self.params.dropzone_add_css.clone()); + let dropzone_css = &self.params.dropzone_css + &self.params.dropzone_add_css; dom! {
diff --git a/src/form/data/data_field.rs b/src/form/data/data_field.rs new file mode 100644 index 0000000..ea54d47 --- /dev/null +++ b/src/form/data/data_field.rs @@ -0,0 +1,103 @@ +use std::{collections::HashMap, rc::Rc}; +use vertigo::{Computed, Context, DomNode, DropFileItem, Value}; + +use crate::DropImageFileParams; + +use super::form_export::FieldExport; + +/// Value of a field in form section. +#[derive(Clone)] +pub enum DataFieldValue { + /// Regular string field. + String(StringValue), + /// Textarea string field. + TextArea(TextAreaValue), + /// String field with options. + List(ListValue), + /// Integer (foreign key) field with labels for each integer. + Dict(DictValue), + /// Array of integers (foreign key) field with labels for each integer. + Multi(MultiValue), + /// Checkbox + Bool(BoolValue), + /// Image (bytes) field. + Image(ImageValue), + /// Custom field + Custom(CustomValue), + /// Custom component without value + StaticCustom(Rc DomNode>), +} + +impl DataFieldValue { + pub fn export(&self, ctx: &Context) -> FieldExport { + match self { + Self::String(val) => FieldExport::String(val.value.get(ctx)), + Self::TextArea(val) => FieldExport::String(val.value.get(ctx)), + Self::List(val) => FieldExport::List(val.value.get(ctx)), + Self::Dict(val) => FieldExport::Dict(val.value.get(ctx)), + Self::Multi(val) => { + FieldExport::Multi(val.value.get(ctx).iter().map(|v| v.get(ctx)).collect()) + } + Self::Bool(val) => FieldExport::Bool(val.value.get(ctx)), + Self::Image(val) => FieldExport::Image((val.original_link.clone(), val.value.get(ctx))), + Self::Custom(val) => FieldExport::String(val.value.get(ctx)), + Self::StaticCustom(_) => FieldExport::String("".to_string()), + } + } +} + +#[derive(Clone)] +pub struct StringValue { + pub value: Value, + pub original_value: Rc, +} + +#[derive(Clone)] +pub struct TextAreaValue { + pub value: Value, + pub original_value: Option>, + pub rows: Option, + pub cols: Option, +} + +#[derive(Clone)] +pub struct ListValue { + pub value: Value, + pub original_value: Option>, + pub options: Computed>, +} + +#[derive(Clone)] +pub struct DictValue { + pub value: Value, + pub original_value: Option>, + pub options: Computed>, +} + +#[derive(Clone)] +pub struct MultiValue { + pub value: Value>>, + pub original_value: Rc>, + pub options: Computed>, + pub add_label: Rc, +} + +#[derive(Clone)] +pub struct BoolValue { + pub value: Value, + pub original_value: Option>, +} + +#[derive(Clone)] +pub struct ImageValue { + pub value: Value>, + pub original_link: Option>, + pub component_params: Option, +} + +#[derive(Clone)] +pub struct CustomValue { + pub value: Value, + pub original_value: Option>, + pub render: Rc DomNode>, +} diff --git a/src/form/data/form_data.rs b/src/form/data/form_data.rs new file mode 100644 index 0000000..43afe91 --- /dev/null +++ b/src/form/data/form_data.rs @@ -0,0 +1,346 @@ +use std::{collections::HashMap, rc::Rc}; +use vertigo::{Computed, Css, DomElement, Value, transaction}; + +use super::{ + DataFieldValue, FormExport, + data_field::{BoolValue, DictValue, ImageValue, ListValue, MultiValue, StringValue}, +}; + +/// Used to define structure of a [Form](super::Form). +/// +/// Example: +/// +/// ```rust +/// use vertigo_forms::form::{DataSection, FieldsetStyle, FormData}; +/// +/// #[derive(Clone, PartialEq)] +/// pub struct MyModel { +/// pub slug: String, +/// pub name: String, +/// pub dimension_x: String, +/// pub dimension_y: String, +/// } +/// +/// impl From<&MyModel> for FormData { +/// fn from(value: &MyModel) -> Self { +/// Self::default() +/// .with(DataSection::with_string_field("Slug", "slug", &value.slug)) +/// .with(DataSection::with_string_field("Name", "name", &value.name)) +/// .with( +/// DataSection::with_string_field("Dimensions", "dimension_x", &value.dimension_x) +/// .add_string_field("dimension_y", &value.dimension_y) +/// .set_fieldset_style(FieldsetStyle::Dimensions), +/// ) +/// } +/// } +/// ``` +/// +/// See story book for more examples. +#[derive(Default)] +pub struct FormData { + pub sections: Vec, + pub tabs: Vec<(String, Rc>)>, + pub top_controls: ControlsConfig, + pub bottom_controls: ControlsConfig, +} + +#[derive(Default)] +pub struct ControlsConfig { + pub css: Option, + pub submit: bool, + pub delete: bool, +} + +impl ControlsConfig { + pub fn full() -> Self { + Self { + css: None, + submit: true, + delete: true, + } + } + + pub fn with_css(mut self, css: Css) -> Self { + self.css = Some(css); + self + } +} + +impl FormData { + /// Add new data section (outside of tabs) + pub fn with(mut self, section: DataSection) -> Self { + self.sections.push(section); + self + } + + /// Add new tab with sections + pub fn add_tab(mut self, tab_label: impl Into, sections: Vec) -> Self { + self.tabs.push((tab_label.into(), Rc::new(sections))); + self + } + + pub fn add_top_controls(mut self) -> Self { + self.top_controls = ControlsConfig::full(); + self + } + + pub fn add_top_controls_styled(mut self, css: Css) -> Self { + self.top_controls = ControlsConfig::full().with_css(css); + self + } + + pub fn add_bottom_controls(mut self) -> Self { + self.bottom_controls = ControlsConfig::full(); + self + } + + pub fn add_bottom_controls_styled(mut self, css: Css) -> Self { + self.bottom_controls = ControlsConfig::full().with_css(css); + self + } + + pub fn export(&self) -> FormExport { + let mut hash_map = HashMap::new(); + transaction(|ctx| { + for (_, sections) in &self.tabs { + for section in sections.iter() { + for field in §ion.fields { + hash_map.insert(field.key.clone(), field.value.export(ctx)); + } + } + } + for section in &self.sections { + for field in §ion.fields { + hash_map.insert(field.key.clone(), field.value.export(ctx)); + } + } + }); + FormExport::new(hash_map) + } +} + +/// Presets for rendering fields in a field set. +#[derive(Clone, Copy, Default, PartialEq)] +pub enum FieldsetStyle { + /// Just one after another (piled) + #[default] + Plain, + /// Interspersed with "x" character + Dimensions, +} + +/// A section of form with label and a field (or field set). +#[derive(Default)] +pub struct DataSection { + pub label: String, + pub fields: Vec, + pub error: Option, + pub render: Option) -> DomElement>>, + pub fieldset_style: FieldsetStyle, + pub fieldset_css: Option, + pub new_group: bool, +} + +/// A single field in form section. +#[derive(Clone)] +pub struct DataField { + pub key: String, + pub value: DataFieldValue, +} + +impl DataSection { + /// Create a new form section without fields. + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + ..Default::default() + } + } + + /// Create a new form section with single string field. + pub fn with_string_field( + label: impl Into, + key: impl Into, + original_value: impl Into, + ) -> Self { + let value = original_value.into(); + Self { + label: label.into(), + fields: vec![DataField { + key: key.into(), + value: DataFieldValue::String(StringValue { + value: Value::new(value.clone()), + original_value: Rc::new(value), + }), + }], + ..Default::default() + } + } + + /// Create a new form section with single optional string field. + pub fn with_opt_string_field( + label: impl Into, + key: impl Into, + original_value: &Option, + ) -> Self { + Self::with_string_field(label, key, original_value.clone().unwrap_or_default()) + } + + pub fn add_field(mut self, key: impl Into, value: DataFieldValue) -> Self { + self.fields.push(DataField { + key: key.into(), + value, + }); + self + } + + /// Add another string field to form section (text input). + pub fn add_string_field( + mut self, + key: impl Into, + original_value: impl Into, + ) -> Self { + let value = original_value.into(); + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::String(StringValue { + value: Value::new(value.clone()), + original_value: Rc::new(value), + }), + }); + self + } + + /// Add another optional string field to form section (text input). + pub fn add_opt_string_field( + self, + key: impl Into, + original_value: &Option, + ) -> Self { + self.add_string_field(key, original_value.clone().unwrap_or_default()) + } + + /// Add another list field to form section (dropdown with options). + pub fn add_list_field( + mut self, + key: impl Into, + original_value: Option>, + options: Vec, + ) -> Self { + let value = original_value.map(|s| s.into()); + let options = Computed::from(move |_ctx| options.clone()); + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::List(ListValue { + value: Value::new(value.clone().unwrap_or_default()), + original_value: value.map(Rc::new), + options, + }), + }); + self + } + + /// Add another dict field to form section based on static (non-reactive) dictionary + /// Renders dropdown with options, value are stored as integer. + pub fn add_static_dict_field( + self, + key: impl Into, + original_value: Option, + options: Vec<(i64, String)>, + ) -> Self { + let options = Computed::from(move |_ctx| options.clone()); + self.add_dict_field(key, original_value, options) + } + + /// Add another dict field to form section based on reactive dictionary + /// Renders dropdown with options, value are stored as integer. + pub fn add_dict_field( + mut self, + key: impl Into, + original_value: Option, + options: Computed>, + ) -> Self { + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::Dict(DictValue { + value: Value::new(original_value.unwrap_or_default()), + original_value: original_value.map(Rc::new), + options, + }), + }); + self + } + + /// Add multiselect field to form section (multiple search inputs, value stored as integer). + pub fn add_multiselect_field( + mut self, + key: impl Into, + original_value: Vec, + options: Computed>, + add_label: impl Into, + ) -> Self { + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::Multi(MultiValue { + value: Value::new(original_value.iter().cloned().map(Value::new).collect()), + original_value: Rc::new(original_value), + options, + add_label: Rc::new(add_label.into()), + }), + }); + self + } + + /// Add another bool field to form section (checkbox input). + pub fn add_bool_field( + mut self, + key: impl Into, + original_value: Option>, + ) -> Self { + let value = original_value.map(|b| b.into()); + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::Bool(BoolValue { + value: Value::new(value.unwrap_or_default()), + original_value: value.map(Rc::new), + }), + }); + self + } + + /// Add another image field to form section. + pub fn add_image_field( + mut self, + key: impl Into, + original_value: Option>, + ) -> Self { + let value = original_value.map(|l| l.into()); + self.fields.push(DataField { + key: key.into(), + value: DataFieldValue::Image(ImageValue { + value: Value::new(None), + original_link: value.map(Rc::new), + component_params: None, + }), + }); + self + } + + /// Set [FieldsetStyle] for this section. + pub fn set_fieldset_style(mut self, fieldset_style: FieldsetStyle) -> Self { + self.fieldset_style = fieldset_style; + self + } + + /// Set [Css] for fields container for this section. + pub fn set_fieldset_css(mut self, fieldset_css: Css) -> Self { + self.fieldset_css = Some(fieldset_css); + self + } + + /// This section starts a new section group (Form adds a horizontal rule) + pub fn starts_new_group(mut self) -> Self { + self.new_group = true; + self + } +} diff --git a/src/form/data/form_export.rs b/src/form/data/form_export.rs new file mode 100644 index 0000000..5669b91 --- /dev/null +++ b/src/form/data/form_export.rs @@ -0,0 +1,145 @@ +use std::{collections::HashMap, rc::Rc}; +use vertigo::DropFileItem; + +use crate::{image_as_uri, nonify}; + +pub enum FieldExport { + Bool(bool), + String(String), + List(String), + Dict(i64), + Multi(Vec), + Image((Option>, Option)), +} + +/// After form is submitted, it generates an export from every field. This can be used to construct a new model. +#[derive(Clone)] +pub struct FormExport(Rc>); + +impl FormExport { + pub fn new(map: HashMap) -> Self { + Self(Rc::new(map)) + } + + pub fn get<'a>(&'a self, key: &str) -> Option<&'a FieldExport> { + self.0.get(key) + } + + /// Get value from string input. + pub fn get_string(&self, key: &str) -> String { + self.get_string_opt(key).unwrap_or_default() + } + + /// Get value from string input or None if empty. + pub fn get_string_opt(&self, key: &str) -> Option { + self.get(key).and_then(|export| { + if let FieldExport::String(val) = export { + nonify(val.clone()) + } else { + None + } + }) + } + + // Get optional value from list field (string-based). + pub fn list>(&self, key: &str) -> Option { + self.get(key).and_then(|export| { + if let FieldExport::List(val) = export { + nonify(val.clone()).map(Into::into) + } else { + None + } + }) + } + // Get value from list field (string-based) or default. + pub fn list_or_default>(&self, key: &str) -> T { + self.list(key).unwrap_or_default() + } + + // Get value from dict field (i64-based). + pub fn dict_or_default>(&self, key: &str) -> T { + self.get(key) + .map(|export| { + if let FieldExport::Dict(val) = export { + *val + } else { + Default::default() + } + }) + .unwrap_or_default() + .into() + } + + // Get values from multi field (i64-based). + pub fn multi>(&self, key: &str) -> Vec { + self.get(key) + .map(|export| { + if let FieldExport::Multi(val) = export { + val.iter().cloned().map(Into::into).collect() + } else { + Default::default() + } + }) + .unwrap_or_default() + } + + /// Get value from bool input (i. e. checkbox) or false. + pub fn get_bool(&self, key: &str) -> bool { + self.get_bool_opt(key).unwrap_or_default() + } + + /// Get value from bool input (i. e. checkbox) or None. + pub fn get_bool_opt(&self, key: &str) -> Option { + self.get(key).and_then(|export| { + if let FieldExport::Bool(val) = export { + Some(*val) + } else { + None + } + }) + } + + // Get new image (base64) or original value from image field. + pub fn image_url(&self, key: &str) -> String { + self.image_url_opt(key).unwrap_or_default() + } + + // Get new image (base64) or original value from image field or none. + pub fn image_url_opt(&self, key: &str) -> Option { + if let Some(export) = self.get(key) { + match export { + FieldExport::Image((orig_link, dfi)) => dfi + .as_ref() + .map(image_as_uri) + .or_else(|| orig_link.as_ref().map(|ol| ol.to_string())), + _ => None, + } + } else { + None + } + } + + // Get new image (base64) + pub fn image_item_opt(&self, key: &str) -> Option { + if let Some(export) = self.get(key) { + match export { + FieldExport::Image((_orig_link, dfi)) => dfi.clone(), + _ => None, + } + } else { + None + } + } + + // Get original image url + pub fn image_orig_url_opt(&self, key: &str) -> Option> { + if let Some(export) = self.get(key) { + match export { + FieldExport::Image((orig_link, _dfi)) => orig_link.clone(), + _ => None, + } + } else { + None + } + } +} diff --git a/src/form/data/mod.rs b/src/form/data/mod.rs new file mode 100644 index 0000000..b6eea43 --- /dev/null +++ b/src/form/data/mod.rs @@ -0,0 +1,30 @@ +use std::rc::Rc; +use vertigo::Value; + +use crate::ValidationErrors; + +mod data_field; +pub use data_field::{DataFieldValue, ImageValue, TextAreaValue}; + +mod form_export; +pub use form_export::{FieldExport, FormExport}; + +mod form_data; +pub use form_data::{ControlsConfig, DataField, DataSection, FieldsetStyle, FormData}; + +pub type ValidateFunc = Rc) -> bool>; + +#[derive(Default, Clone, PartialEq)] +pub enum Operation { + #[default] + None, + Saving, + Success, + Error(Rc), +} + +impl Operation { + pub fn err(message: impl Into) -> Self { + Self::Error(Rc::new(message.into())) + } +} diff --git a/src/form/field.rs b/src/form/field.rs deleted file mode 100644 index 87d39bd..0000000 --- a/src/form/field.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{collections::HashMap, rc::Rc}; -use vertigo::{Computed, Context, DropFileItem, Value}; - -use crate::image_as_uri; - -#[derive(Clone)] -pub struct StringValue { - pub value: Value, - pub original_value: Rc, -} - -#[derive(Clone)] -pub struct ListValue { - pub value: Value, - pub original_value: Rc, - pub options: Computed>, -} - -#[derive(Clone)] -pub struct DictValue { - pub value: Value, - pub original_value: Rc, - pub options: Computed>, -} - -#[derive(Clone)] -pub struct ImageValue { - pub value: Value>, - pub original_link: Option>, -} - -/// Value of a field in form section. -#[derive(Clone)] -pub enum DataFieldValue { - /// Regular string field. - String(StringValue), - /// String field with options. - List(ListValue), - /// Integer (foreign key) field with labels for each integer. - Dict(DictValue), - /// Image (bytes) field. - Image(ImageValue), -} - -impl DataFieldValue { - pub fn export(&self, ctx: &Context) -> FieldExport { - match self { - Self::String(val) => FieldExport::String(val.value.get(ctx)), - Self::List(val) => FieldExport::List(val.value.get(ctx)), - Self::Dict(val) => FieldExport::Dict(val.value.get(ctx)), - Self::Image(val) => FieldExport::Image((val.original_link.clone(), val.value.get(ctx))), - } - } -} - -pub enum FieldExport { - String(String), - List(String), - Dict(i64), - Image((Option>, Option)), -} - -/// After form is submitted, it generates an export from every field. This can be used to construct a new model. -pub struct FormExport(HashMap); - -impl FormExport { - pub fn new(map: HashMap) -> Self { - Self(map) - } - - pub fn get<'a>(&'a self, key: &str) -> Option<&'a FieldExport> { - self.0.get(key) - } - - /// Get value from string input. - pub fn get_string(&self, key: &str) -> String { - self.get(key) - .map(|export| { - if let FieldExport::String(val) = export { - val.clone() - } else { - Default::default() - } - }) - .unwrap_or_default() - } - - // Get value from list field (string). - pub fn list_or_default(&self, key: &str) -> String { - self.get(key) - .map(|export| { - if let FieldExport::List(val) = export { - val.clone() - } else { - Default::default() - } - }) - .unwrap_or_default() - } - - // Get value from dict field (i64). - pub fn dict_or_default(&self, key: &str) -> i64 { - self.get(key) - .map(|export| { - if let FieldExport::Dict(val) = export { - *val - } else { - Default::default() - } - }) - .unwrap_or_default() - } - - // Get new image (base64) or original value from image field. - pub fn image_url(&self, key: &str) -> String { - if let Some(export) = self.get(key) { - match export { - FieldExport::Image((orig_link, dfi)) => { - dfi.as_ref().map(image_as_uri).unwrap_or_else(|| { - orig_link - .as_ref() - .map(|ol| ol.to_string()) - .unwrap_or_default() - }) - } - _ => String::default(), - } - } else { - String::default() - } - } -} diff --git a/src/form/form_data.rs b/src/form/form_data.rs deleted file mode 100644 index 0f730c7..0000000 --- a/src/form/form_data.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::{collections::HashMap, rc::Rc}; -use vertigo::{transaction, Computed, DomElement, Value}; - -use super::{ - field::{DictValue, ImageValue, ListValue, StringValue}, - DataFieldValue, FormExport, -}; - -/// Used to define structure of a [Form](super::Form). -/// -/// Example: -/// -/// ```rust -/// use vertigo_forms::form::{DataSection, FieldsetStyle, FormData}; -/// -/// #[derive(Clone, PartialEq)] -/// pub struct MyModel { -/// pub slug: String, -/// pub name: String, -/// pub dimension_x: String, -/// pub dimension_y: String, -/// } -/// -/// impl From<&MyModel> for FormData { -/// fn from(value: &MyModel) -> Self { -/// Self::default() -/// .with(DataSection::with_string_field("Slug", "slug", &value.slug)) -/// .with(DataSection::with_string_field("Name", "name", &value.name)) -/// .with( -/// DataSection::with_string_field("Dimensions", "dimension_x", &value.dimension_x) -/// .add_string_field("dimension_y", &value.dimension_y) -/// .set_fieldset_style(FieldsetStyle::Dimensions), -/// ) -/// } -/// } -/// ``` -/// -/// See story book for more examples. -#[derive(Default)] -pub struct FormData { - pub sections: Vec, -} - -impl FormData { - /// Add new data section - pub fn with(mut self, section: DataSection) -> Self { - self.sections.push(section); - self - } - - pub fn export(&self) -> FormExport { - let mut hash_map = HashMap::new(); - transaction(|ctx| { - for section in &self.sections { - for field in §ion.fields { - hash_map.insert(field.key.clone(), field.value.export(ctx)); - } - } - }); - FormExport::new(hash_map) - } -} - -/// Presets for rendering fields in a field set. -#[derive(Clone, Copy, Default, PartialEq)] -pub enum FieldsetStyle { - /// Just one after another (piled) - #[default] - Plain, - /// Interspersed with "x" character - Dimensions, -} - -/// A section of form with label and a field (or field set). -#[derive(Default)] -pub struct DataSection { - pub label: String, - pub fields: Vec, - pub error: Option, - pub render: Option) -> DomElement>>, - pub fieldset_style: FieldsetStyle, -} - -/// A single field in form section. -pub struct DataField { - pub key: String, - pub value: DataFieldValue, -} - -impl DataSection { - /// Create a new form section without fields. - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - ..Default::default() - } - } - - /// Create a new form section with single string field. - pub fn with_string_field( - label: impl Into, - key: impl Into, - original_value: impl Into, - ) -> Self { - let value = original_value.into(); - Self { - label: label.into(), - fields: vec![DataField { - key: key.into(), - value: DataFieldValue::String(StringValue { - value: Value::new(value.clone()), - original_value: Rc::new(value), - }), - }], - ..Default::default() - } - } - - /// Add another string field to form section (text input). - pub fn add_string_field( - mut self, - key: impl Into, - original_value: impl Into, - ) -> Self { - let value = original_value.into(); - self.fields.push(DataField { - key: key.into(), - value: DataFieldValue::String(StringValue { - value: Value::new(value.clone()), - original_value: Rc::new(value), - }), - }); - self - } - - /// Add another list field to form section (dropdown with options). - pub fn add_list_field( - mut self, - key: impl Into, - original_value: impl Into, - options: Vec, - ) -> Self { - let value = original_value.into(); - let options = Computed::from(move |_ctx| options.clone()); - self.fields.push(DataField { - key: key.into(), - value: DataFieldValue::List(ListValue { - value: Value::new(value.clone()), - original_value: Rc::new(value), - options, - }), - }); - self - } - - /// Add another dict field to form section (dropdown with options, value stored as integer). - pub fn add_dict_field( - mut self, - key: impl Into, - original_value: i64, - options: Vec<(i64, String)>, - ) -> Self { - let options = Computed::from(move |_ctx| options.clone()); - self.fields.push(DataField { - key: key.into(), - value: DataFieldValue::Dict(DictValue { - value: Value::new(original_value), - original_value: Rc::new(original_value), - options, - }), - }); - self - } - - /// Add another image field to form section. - pub fn add_image_field( - mut self, - key: impl Into, - original_value: Option>, - ) -> Self { - self.fields.push(DataField { - key: key.into(), - value: DataFieldValue::Image(ImageValue { - value: Value::new(None), - original_link: original_value.map(|link| Rc::new(link.into())), - }), - }); - self - } - - /// Set [FieldsetStyle] for this section. - pub fn set_fieldset_style(mut self, fieldset_style: FieldsetStyle) -> Self { - self.fieldset_style = fieldset_style; - self - } -} diff --git a/src/form/mod.rs b/src/form/mod.rs index 68b4885..f48f637 100644 --- a/src/form/mod.rs +++ b/src/form/mod.rs @@ -6,22 +6,33 @@ //! See story book for examples. use std::rc::Rc; -use vertigo::{bind_rc, component, css, dom, Css}; +use vertigo::{AttrGroup, Css, Value, bind, bind_rc, component, css, dom}; -use crate::{input::NamedInput, DictSelect, DropImageFile, DropImageFileParams, Select}; +use crate::{TabsParams, ValidationErrors}; -mod field; -pub use field::{DataFieldValue, FieldExport, FormExport}; -mod form_data; -pub use form_data::{DataField, DataSection, FieldsetStyle, FormData}; +mod data; +pub use data::*; -pub struct FormParams { +mod render; +pub use render::*; + +#[derive(Clone)] +pub struct FormParams { pub css: Css, pub add_css: Css, - pub submit_label: String, + pub add_section_css: Css, + pub submit_label: Rc, + pub on_delete: Option>, + pub delete_label: Rc, + pub validate: Option>, + pub validation_errors: Value, + pub operation: Value, + pub saving_label: Rc, + pub saved_label: Rc, + pub tabs_params: Option, } -impl Default for FormParams { +impl Default for FormParams { fn default() -> Self { Self { css: css! { " @@ -30,35 +41,16 @@ impl Default for FormParams { gap: 5px; " }, add_css: Css::default(), - submit_label: "Submit".to_string(), - } - } -} - -#[component] -pub fn Field<'a>(field: &'a DataField) { - match &field.value { - DataFieldValue::String(val) => { - dom! { } - } - DataFieldValue::List(val) => { - dom! { + }); + } + if c_config.delete + && let Some(on_click) = params.on_delete.clone() + { + controls.push(dom! { + + }); + } - let fields = form_data.sections.iter().map(|section| { - if section.fields.len() > 1 { - let mut values = vec![]; - for (i, field) in section.fields.iter().enumerate() { - if section.fieldset_style == FieldsetStyle::Dimensions && i > 0 { - values.push(dom! { "x" }); + let errors = validation_errors + .render_value_option(|errs| errs.get("submit").map(|err| dom! { {err} })); + + let operation_str = bind!( + params.saving_label, + params.saved_label, + params.operation.render_value_option(move |oper| { + let mut css = ctrl_item_css.clone(); + match oper { + Operation::Saving => Some(saving_label.clone()), + Operation::Success => Some(saved_label.clone()), + Operation::Error(err) => { + css += css! {"color: red;"}; + Some(err) + } + _ => None, } - values.push(dom! { }); - } + .map(|operation_str| dom! { {operation_str} }) + }) + ); - let label = §ion.label; - dom! { - - } - } else if let Some(field) = section.fields.first() { - dom! { - + if controls.is_empty() { + None + } else { + let mut css_controls = css!("grid-column: span 2;"); + if let Some(custom_css) = &c_config.css { + css_controls += custom_css; } + Some(dom! { +
+ {..controls} + {errors} + {operation_str} +
+ }) + } + }; + + let top_controls = controls(¶ms, &form_data.top_controls); + let bottom_controls = controls(¶ms, &form_data.bottom_controls); + + let section_css = subgrid_css + params.add_section_css; + + let fields = fields( + &form_data.sections, + &s, + validation_errors.clone(), + §ion_css, + ); + + let tabs = tabs( + &form_data.tabs, + ¶ms.tabs_params, + &s, + validation_errors.clone(), + §ion_css, + ¶ms.css.clone(), + ); + + let form_css = params.css + params.add_css; + + let on_submit = bind_rc!(form_data, validation_errors, || { + params.operation.set(Operation::Saving); + let model = form_data.export(); + let valid = if let Some(validate) = ¶ms.validate { + validate(&model.clone().into(), validation_errors.clone()) } else { - dom! {

} + true + }; + if valid { + on_submit(model); } }); - let on_submit = bind_rc!(form_data, || { - on_submit(form_data.export()); - }); - - let form_css = params.css.extend(params.add_css.clone()); - dom! { -

+ + {..top_controls} {..fields} - + {..tabs} + {..bottom_controls}
} } diff --git a/src/form/render/field.rs b/src/form/render/field.rs new file mode 100644 index 0000000..2574921 --- /dev/null +++ b/src/form/render/field.rs @@ -0,0 +1,81 @@ +use vertigo::{DomElement, Value, bind, component, css, dom, dom_element}; + +use crate::{DictSelect, DropImageFile, Select, SelectSearch, Switch, SwitchParams, input::Input}; + +use super::super::{DataField, DataFieldValue}; + +#[component] +pub fn Field<'a>(field: &'a DataField) { + match &field.value { + DataFieldValue::String(val) => { + dom! { } + } + DataFieldValue::TextArea(val) => { + let on_input = bind!(val.value, |new_value: String| { + value.set(new_value); + }); + let el = + dom_element! { }; + if let Some(rows) = val.rows { + el.add_attr("rows", rows); + } + if let Some(cols) = val.cols { + el.add_attr("cols", cols); + } + el.into() + } + DataFieldValue::List(val) => { + dom! { - } +#[component] +pub fn Input(value: Value, input: AttrGroup) { + let on_input = bind!(value, |new_value: String| { + value.set(new_value); + }); + + dom! { + } } diff --git a/src/input/input_with_button.rs b/src/input/input_with_button.rs index 65ad3b6..1d39a3d 100644 --- a/src/input/input_with_button.rs +++ b/src/input/input_with_button.rs @@ -1,4 +1,4 @@ -use vertigo::{bind, computed_tuple, dom, transaction, Css, DomNode, Value}; +use vertigo::{AttrGroup, Value, bind, component, computed_tuple, dom, transaction}; /// Input connected to provided `Value`. /// @@ -23,64 +23,53 @@ use vertigo::{bind, computed_tuple, dom, transaction, Css, DomNode, Value}; /// /// }; /// ``` -pub struct InputWithButton { - pub value: Value, - pub params: InputWithButtonParams, +#[component] +pub fn InputWithButton( + value: Value, + params: InputWithButtonParams, + input: AttrGroup, + button: AttrGroup, +) { + let temp_value = Value::>::default(); + let display_value = + computed_tuple!(value, temp_value).map(|(value, temp_value)| temp_value.unwrap_or(value)); + + let on_input = bind!(temp_value, |new_value: String| { + temp_value.set(Some(new_value)); + }); + + let on_click = bind!(value, temp_value, |_| { + transaction(|ctx| { + let new_value = temp_value.get(ctx); + if let Some(new_value) = new_value { + value.set(new_value); + } + temp_value.set(None); + }) + }); + + dom! { + + + } } #[derive(Clone)] pub struct InputWithButtonParams { - pub input_css: Css, pub button_label: String, - pub button_css: Css, } impl Default for InputWithButtonParams { fn default() -> Self { Self { - input_css: Css::default(), button_label: "OK".to_string(), - button_css: Css::default(), - } - } -} - -impl InputWithButton { - pub fn into_component(self) -> Self { - self - } - - pub fn mount(self) -> DomNode { - let Self { value, params } = self; - - let temp_value = Value::>::default(); - let display_value = computed_tuple!(value, temp_value) - .map(|(value, temp_value)| temp_value.unwrap_or(value)); - - let on_input = bind!(temp_value, |new_value: String| { - temp_value.set(Some(new_value)); - }); - - let on_click = bind!(value, temp_value, |_| { - transaction(|ctx| { - let new_value = temp_value.get(ctx); - if let Some(new_value) = new_value { - value.set(new_value); - } - temp_value.set(None); - }) - }); - - dom! { - - } } } diff --git a/src/input/list_input.rs b/src/input/list_input.rs index 5090217..db26830 100644 --- a/src/input/list_input.rs +++ b/src/input/list_input.rs @@ -1,40 +1,29 @@ -use vertigo::{bind, dom, DomNode, Value}; +use vertigo::{AttrGroup, Value, bind, component, dom}; /// Input connected to provided `Value>`. /// /// It parsed a comma-separated input string and sets value as a vector. -pub struct ListInput { - pub value: Value>, -} - -impl ListInput { - pub fn into_component(self) -> Self { - self - } - - pub fn mount(self) -> DomNode { - let Self { value } = self; - - let value_str = value.map(|v| v.join(",")); +#[component] +pub fn ListInput(value: Value>, input: AttrGroup) { + let value_str = value.map(|v| v.join(",")); - let on_input = bind!(value, |new_value: String| { - value.set( - new_value - .split(',') - .filter_map(|v| { - let v = v.trim(); - if v.is_empty() { - None - } else { - Some(v.to_string()) - } - }) - .collect(), - ); - }); + let on_input = bind!(value, |new_value: String| { + value.set( + new_value + .split(',') + .filter_map(|v| { + let v = v.trim(); + if v.is_empty() { + None + } else { + Some(v.to_string()) + } + }) + .collect(), + ); + }); - dom! { - - } + dom! { + } } diff --git a/src/lib.rs b/src/lib.rs index 2ec8bd1..cb22d0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ mod tabs; mod with_loader; pub use { - drop_image_file::{image_as_uri, name_to_mime, DropImageFile, DropImageFileParams}, + drop_image_file::{DropImageFile, DropImageFileParams, image_as_uri, name_to_mime}, input::{Input, InputWithButton, InputWithButtonParams, ListInput}, popup::{Popup, PopupOnHover, PopupParams}, search_panel::{SearchPanel, SearchPanelParams, SearchResult}, @@ -23,7 +23,7 @@ pub use { spinner::Spinner, switch::{Switch, SwitchParams}, tabs::{Tab, Tabs, TabsContent, TabsContentMapped, TabsHeader, TabsParams}, - with_loader::{with_loader, WithLoader}, + with_loader::{WithLoader, with_loader}, }; pub type ValidationErrors = HashMap; diff --git a/src/login.rs b/src/login.rs index f6a8a71..ef1d488 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,5 +1,5 @@ use std::rc::Rc; -use vertigo::{bind, bind_rc, css, dom, transaction, Css, DomNode, KeyDownEvent, Resource, Value}; +use vertigo::{Css, DomNode, KeyDownEvent, Resource, Value, bind, bind_rc, css, dom, transaction}; pub type OnSubmit = Rc; @@ -14,6 +14,7 @@ pub struct LoginParams { pub add_css: Css, pub line_css: Css, pub line_add_css: Css, + pub input_css: Css, pub submit_css: Css, pub submit_add_css: Css, pub error_message: Rc String>, @@ -38,6 +39,7 @@ impl Default for LoginParams { margin-bottom: 5px; "}, line_add_css: Css::default(), + input_css: Css::default(), submit_css: css! {" margin-top: 15px; "}, @@ -78,12 +80,9 @@ impl Login { false }); - let css = params.css.clone().extend(params.add_css.clone()); - let line_css = params.line_css.clone().extend(params.line_add_css.clone()); - let submit_css = params - .submit_css - .clone() - .extend(params.submit_add_css.clone()); + let css = ¶ms.css + ¶ms.add_css; + let line_css = ¶ms.line_css + ¶ms.line_add_css; + let submit_css = ¶ms.submit_css + ¶ms.submit_add_css; let error_message = params.error_message.clone(); let waiting_label = params.waiting_label.clone(); @@ -111,7 +110,11 @@ impl Login { let username_div = dom! {
{¶ms.username_label}
- +
}; @@ -120,7 +123,12 @@ impl Login { let password_div = dom! {
{¶ms.password_label}
- +
}; diff --git a/src/popup.rs b/src/popup.rs index 4686baa..2d69a14 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -1,4 +1,4 @@ -use vertigo::{bind, component, css, dom, Computed, Css, DomNode}; +use vertigo::{Computed, Css, DomNode, bind, component, css, dom}; #[derive(Clone, Default)] pub struct PopupParams { @@ -24,16 +24,14 @@ fn operator_css() -> Css { pub fn Popup(visible: Computed, content: DomNode, params: PopupParams) { let popup_css = popup_css(); - let operator_css = bind!( + let container_css = bind!( popup_css, params, visible.map(move |enabled| { let base_css = operator_css(); if enabled { - base_css.extend(css! {" - [popup_css] { visibility: visible; } - "}) + base_css + css! {"[popup_css] { visibility: visible; }"} } else { base_css } @@ -41,8 +39,8 @@ pub fn Popup(visible: Computed, content: DomNode, params: PopupParams) { ); dom! { -
-
+
+
{content}
@@ -53,14 +51,12 @@ pub fn Popup(visible: Computed, content: DomNode, params: PopupParams) { pub fn PopupOnHover(element: DomNode, content: DomNode, params: PopupParams) { let popup_css = popup_css(); - let operator_css = operator_css().extend(css! {" - :hover [popup_css] { visibility: visible; } - "}); + let operator_css = operator_css() + css! {":hover [popup_css] { visibility: visible; }"}; dom! {
{element} -
+
{content}
diff --git a/src/search_panel.rs b/src/search_panel.rs index 286ef36..c919d7c 100644 --- a/src/search_panel.rs +++ b/src/search_panel.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use vertigo::{bind, dom, AutoMap, DomNode, Resource, ToComputed, Value}; +use vertigo::{AutoMap, DomNode, Resource, ToComputed, Value, bind, dom}; pub trait SearchResult { fn is_empty(&self) -> bool; diff --git a/src/select/dict_select.rs b/src/select/dict_select.rs index 392bcff..adc218d 100644 --- a/src/select/dict_select.rs +++ b/src/select/dict_select.rs @@ -1,4 +1,4 @@ -use vertigo::{bind, dom, Computed, DomNode, Value}; +use vertigo::{AttrGroup, Computed, Value, bind, component, computed_tuple, dom}; /// Simple Select component based on map of `i64`->`T` values. /// @@ -23,44 +23,42 @@ use vertigo::{bind, dom, Computed, DomNode, Value}; /// /> /// }; /// ``` -pub struct DictSelect { - pub value: Value, - pub options: Computed>, -} - -impl DictSelect -where - T: Clone + From + PartialEq + ToString + 'static, -{ - pub fn into_component(self) -> Self { - self - } +#[component] +pub fn DictSelect + PartialEq + ToString + 'static>( + value: Value, + options: Computed>, + select: AttrGroup, +) { + let on_change = bind!(value, |new_value: String| { + value.set(new_value.parse().unwrap_or_default()); + }); - pub fn mount(&self) -> DomNode { - let Self { value, options } = self; - let on_change = bind!(value, |new_value: String| { - value.set(new_value.parse().unwrap_or_default()); - }); + let empty = computed_tuple!(value, options).render_value_option(|(value, options)| { + options + .iter() + .any(|(key, _)| key != &value) + .then(|| dom! { } - } else { - dom! { } - } + let list = bind!( + options, + value.render_value(move |value| options.render_list( + |(key, _)| key.to_string(), + move |(key, item)| { + let text_item = item.to_string(); + if key == &value { + dom! { } + } else { + dom! { } } - )) - ); + } + )) + ); - dom! { - - } + dom! { + } } diff --git a/src/select/multi_drop_down.rs b/src/select/multi_drop_down.rs index 34ea3bb..a7c986a 100644 --- a/src/select/multi_drop_down.rs +++ b/src/select/multi_drop_down.rs @@ -1,4 +1,4 @@ -use vertigo::{bind, css, dom, Computed, Css, DomNode, Value}; +use vertigo::{Computed, Css, DomNode, Value, bind, css, dom}; use super::multi_select::MultiSelect; @@ -35,12 +35,12 @@ where position: absolute; z-index: 1; "}; - let drop_down_content_css = content_css.extend(self.params.drop_down_content_css); + let drop_down_content_css = content_css + self.params.drop_down_content_css; let content = opened.render_value(move |opened| { if opened { dom! { -
+
}) + }); + let list = bind!( options, value.render_value(move |value| options.render_list( @@ -59,6 +68,7 @@ where dom! { } diff --git a/src/select_search.rs b/src/select_search.rs index a08b96c..b6876ec 100644 --- a/src/select_search.rs +++ b/src/select_search.rs @@ -1,8 +1,8 @@ use either::Either; use std::{collections::HashMap, hash::Hash}; use vertigo::{ - bind, computed_tuple, css, dom, dom_element, transaction, Computed, DomNode, KeyDownEvent, - Value, + Computed, DomNode, KeyDownEvent, Value, bind, computed_tuple, css, dom, dom_element, + transaction, }; /// Input that searches for entered query in provided item list, based on `HashMap`. diff --git a/src/switch.rs b/src/switch.rs index b43e9a1..47deb6e 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -1,4 +1,4 @@ -use vertigo::{bind, dom, transaction, DomNode, Value}; +use vertigo::{AttrGroup, Value, bind, component, dom, transaction}; pub enum DisplayType { Button, @@ -46,60 +46,52 @@ impl SwitchParams { /// /> /// }; /// ``` -pub struct Switch { - pub value: Value, - pub params: SwitchParams, -} - -impl Switch { - pub fn into_component(self) -> Self { - self - } - - pub fn mount(self) -> DomNode { - let Self { value, params } = self; - - let toggle = bind!(value, |_| transaction(|ctx| value.set(!value.get(ctx)))); - - match params.display_type { - DisplayType::Button => { - let symbol = value.map(move |value| { - if value { - params.on_symbol.clone() - } else { - params.off_symbol.clone() - } - }); +#[component] +pub fn Switch( + value: Value, + params: SwitchParams, + i: AttrGroup, // TODO: Use +) { + let toggle = bind!(value, |_| transaction(|ctx| value.set(!value.get(ctx)))); - dom! { - + match params.display_type { + DisplayType::Button => { + let symbol = value.map(move |value| { + if value { + params.on_symbol.clone() + } else { + params.off_symbol.clone() } + }); + + dom! { + } - DisplayType::CheckBox => { - let value_clone = value.clone(); - value.render_value(move |value_inner| { - let toggle = bind!(value_clone, |_| transaction( - |ctx| value_clone.set(!value_clone.get(ctx)) - )); - if value_inner { - dom! { - - } - } else { - dom! { - - } + } + DisplayType::CheckBox => { + let value_clone = value.clone(); + value.render_value(move |value_inner| { + let toggle = bind!(value_clone, |_| transaction( + |ctx| value_clone.set(!value_clone.get(ctx)) + )); + if value_inner { + dom! { + } - }) + } else { + dom! { + + } + } + }) - // Following doesn't work as browsers reads attribute 'checked' only on first render - // let checked = value.map(move |value| - // if value { Some("checked".to_string()) } else { None } - // ); - // dom! { - // - // } - } + // Following doesn't work as browsers reads attribute 'checked' only on first render + // let checked = value.map(move |value| + // if value { Some("checked".to_string()) } else { None } + // ); + // dom! { + // + // } } } } diff --git a/src/tabs/mod.rs b/src/tabs/mod.rs index f81bc99..e824e4c 100644 --- a/src/tabs/mod.rs +++ b/src/tabs/mod.rs @@ -1,5 +1,5 @@ use std::rc::Rc; -use vertigo::{bind, css, dom, Computed, Css, DomElement, DomNode, Reactive, ToComputed}; +use vertigo::{Computed, Css, DomElement, DomNode, Reactive, ToComputed, bind, css, dom}; #[derive(Clone)] pub struct Tab { @@ -8,22 +8,20 @@ pub struct Tab { pub render: Rc DomNode>, } -pub type RenderHeaderFunc = Rc) -> DomNode>; - #[derive(Clone)] -pub struct TabsParams { - pub render_header_item: Option>, +pub struct TabsParams { pub header_css: Css, pub header_item_css: Css, pub header_item_add_css: Css, pub header_active_item_add_css: Css, pub content_css: Css, + pub container_css: Css, } -impl Default for TabsParams { +impl Default for TabsParams { fn default() -> Self { Self { - render_header_item: None, + // render_header_item: None, header_css: css! {" display: flex; flex-wrap: wrap; @@ -37,6 +35,7 @@ impl Default for TabsParams { header_item_add_css: Css::default(), header_active_item_add_css: Css::default(), content_css: Css::default(), + container_css: Css::default(), } } } @@ -45,7 +44,7 @@ impl Default for TabsParams { pub struct Tabs, K: Clone> { pub current_tab: R, pub tabs: Vec>, - pub params: TabsParams, + pub params: TabsParams, } impl Tabs @@ -67,7 +66,7 @@ where let current_computed = current_tab.to_computed(); dom! { -
+
, K: Clone> { pub current_tab: R, pub tabs: Vec>, - pub params: TabsParams, + pub params: TabsParams, } impl TabsHeader @@ -106,7 +105,7 @@ where params, } = self; - let header_item_css = params.header_item_css.extend(params.header_item_add_css); + let header_item_css = params.header_item_css + params.header_item_add_css; let header_active_item_add_css = params.header_active_item_add_css; // let current_tab_clone = current_tab.clone(); @@ -116,27 +115,18 @@ where let header = DomElement::new("ul").css(params.header_css.clone()); tabs.iter().for_each(|tab| { - if let Some(render_header_item) = ¶ms.render_header_item { - // Custom item rendering - header.add_child(render_header_item(tab)); + let on_click = bind!(current_tab, tab | _ | current_tab.set(tab.key.clone())); + let header_item_css = if current_tab_val == tab.key { + &header_item_css + &header_active_item_add_css } else { - // Default item rendering - let on_click = - bind!(current_tab, tab | _ | current_tab.set(tab.key.clone())); - let header_item_css = if current_tab_val == tab.key { - header_item_css - .clone() - .extend(header_active_item_add_css.clone()) - } else { - header_item_css.clone() - }; - let item_css = css!("display: block;"); - header.add_child(dom! { -
  • - {&tab.name} -
  • - }); - } + header_item_css.clone() + }; + let item_css = css!("display: block;"); + header.add_child(dom! { +
  • + {&tab.name} +
  • + }); }); header.into() @@ -148,7 +138,7 @@ where pub struct TabsContent { pub current_tab: Computed, pub tabs: Vec>, - pub params: TabsParams, + pub params: TabsParams, } impl TabsContent @@ -179,7 +169,7 @@ pub struct TabsContentMapped { pub current_tab: Computed, pub tabs: Vec>, pub tab_map: Rc K>, - pub params: TabsParams, + pub params: TabsParams, } impl TabsContentMapped @@ -208,7 +198,7 @@ fn render_tab_content( current_tab: &K, effective_tab: &K, tabs: &[Tab], - params: &TabsParams, + params: &TabsParams, ) -> DomNode { let inner = match tabs.iter().find(|tab| &tab.key == effective_tab).cloned() { Some(tab) => (tab.render)(current_tab), diff --git a/src/with_loader.rs b/src/with_loader.rs index f4ecd38..00a660c 100644 --- a/src/with_loader.rs +++ b/src/with_loader.rs @@ -1,5 +1,5 @@ use std::rc::Rc; -use vertigo::{component, dom, Computed, DomNode, Resource}; +use vertigo::{Computed, DomNode, Resource, component, dom}; use crate::Spinner; diff --git a/storybook/src/form.rs b/storybook/src/form.rs deleted file mode 100644 index 819372b..0000000 --- a/storybook/src/form.rs +++ /dev/null @@ -1,208 +0,0 @@ -use vertigo::{bind_rc, component, css, dom, DomNode, Value}; -use vertigo_forms::form::{ - DataSection, FieldsetStyle, FormData, FormExport, FormParams, ModelForm, -}; - -pub fn form() -> DomNode { - dom! { - -
    - - } -} - -// Form example 1 - -#[derive(Clone, PartialEq)] -pub struct MyModel { - pub slug: String, - pub name: String, - pub dimension_x: String, - pub dimension_y: String, -} - -impl From<&MyModel> for FormData { - fn from(value: &MyModel) -> Self { - Self::default() - .with(DataSection::with_string_field("Slug", "slug", &value.slug)) - .with(DataSection::with_string_field("Name", "name", &value.name)) - .with( - DataSection::with_string_field("Dimensions", "dimension_x", &value.dimension_x) - .add_string_field("dimension_y", &value.dimension_y) - .set_fieldset_style(FieldsetStyle::Dimensions), - ) - } -} - -impl From for MyModel { - fn from(form_export: FormExport) -> Self { - Self { - slug: form_export.get_string("slug"), - name: form_export.get_string("name"), - dimension_x: form_export.get_string("dimension_x"), - dimension_y: form_export.get_string("dimension_y"), - } - } -} - -#[component] -pub fn Form1() { - let my_model: Value = Value::new(MyModel { - slug: "model-one".to_string(), - name: "Model One".to_string(), - dimension_x: "120".to_string(), - dimension_y: "80".to_string(), - }); - - let my_model_clone = my_model.clone(); - let form = my_model.render_value(move |model| { - let on_submit = bind_rc!(my_model_clone, |new_model: MyModel| { - my_model_clone.set(new_model); - }); - - dom! { - - } - }); - - dom! { -
    -

    "Form 1:"

    - {form} -

    "Model 1:"

    -

    {my_model.map(|m| m.slug)} " / " {my_model.map(|m| m.name)}

    -

    {my_model.map(|m| m.dimension_x)} "x" {my_model.map(|m| m.dimension_y)}

    -
    - } -} - -// Form example 2 - -#[derive(Clone, PartialEq)] -pub enum Gender { - Male, - Female, -} - -impl std::fmt::Display for Gender { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Male => write!(f, "Male"), - Self::Female => write!(f, "Female"), - } - } -} - -impl From for Gender { - fn from(value: String) -> Self { - match value.as_str() { - "Female" => Self::Female, - _ => Self::Male, - } - } -} - -#[derive(Clone, PartialEq)] -pub struct MySecondModel { - pub first_name: String, - pub surname: String, - pub gender: Gender, - pub role: i64, - pub photo: String, -} - -impl From<&MySecondModel> for FormData { - fn from(value: &MySecondModel) -> Self { - let gender_map = vec!["Male".to_string(), "Female".to_string()]; - - let role_map = [ - (1i64, "Admin".to_string()), - (2, "Editor".to_string()), - (3, "Reporter".to_string()), - (4, "Viewer".to_string()), - ] - .into(); - - Self { - sections: vec![ - DataSection::with_string_field("First Name", "first_name", &value.first_name), - DataSection::with_string_field("Surname", "surname", &value.surname), - DataSection::new("Gender").add_list_field( - "gender", - value.gender.to_string(), - gender_map, - ), - DataSection::new("Role").add_dict_field("role", value.role, role_map), - DataSection::new("Photo").add_image_field("photo", Some(&value.photo)), - ], - } - } -} - -impl From for MySecondModel { - fn from(form_export: FormExport) -> Self { - Self { - first_name: form_export.get_string("first_name"), - surname: form_export.get_string("surname"), - gender: Gender::from(form_export.list_or_default("gender")), - role: form_export.dict_or_default("role"), - photo: form_export.image_url("photo"), - } - } -} - -#[component] -pub fn Form2() { - let my_second_model: Value = Value::new(MySecondModel { - first_name: "Johann".to_string(), - surname: "Gambolputty".to_string(), - gender: Gender::Male, - role: 1, - photo: "https://picsum.photos/200".to_string(), - }); - - let my_model_clone = my_second_model.clone(); - let form = my_second_model.render_value(move |model| { - let on_submit = bind_rc!(my_model_clone, |new_model: MySecondModel| { - my_model_clone.set(new_model); - }); - - dom! { - - } - }); - - dom! { -
    -

    "Form 2:"

    - {form} -

    "Model 2:"

    -

    - {my_second_model.map(|m| m.first_name)} - " / " - {my_second_model.map(|m| m.surname)} - " (" {my_second_model.map(|m| m.gender.to_string())} ")" -

    -

    "Role: " {my_second_model.map(|m| m.role)}

    -

    - "Photo:"
    - -

    -
    - } -} diff --git a/storybook/src/form/form1.rs b/storybook/src/form/form1.rs new file mode 100644 index 0000000..f01afb2 --- /dev/null +++ b/storybook/src/form/form1.rs @@ -0,0 +1,77 @@ +use vertigo::{bind_rc, component, css, dom, Value}; +use vertigo_forms::form::{ + DataSection, FieldsetStyle, FormData, FormExport, FormParams, ModelForm, +}; + +// Form example 1 + +#[derive(Clone, PartialEq)] +pub struct MyModel { + pub slug: String, + pub name: String, + pub dimension_x: String, + pub dimension_y: String, +} + +impl From<&MyModel> for FormData { + fn from(value: &MyModel) -> Self { + Self::default() + .with(DataSection::with_string_field("Slug", "slug", &value.slug)) + .with(DataSection::with_string_field("Name", "name", &value.name)) + .with( + DataSection::with_string_field("Dimensions", "dimension_x", &value.dimension_x) + .add_string_field("dimension_y", &value.dimension_y) + .set_fieldset_style(FieldsetStyle::Dimensions), + ) + .add_bottom_controls() + } +} + +impl From for MyModel { + fn from(form_export: FormExport) -> Self { + Self { + slug: form_export.get_string("slug"), + name: form_export.get_string("name"), + dimension_x: form_export.get_string("dimension_x"), + dimension_y: form_export.get_string("dimension_y"), + } + } +} + +#[component] +pub fn Form1() { + let my_model: Value = Value::new(MyModel { + slug: "model-one".to_string(), + name: "Model One".to_string(), + dimension_x: "120".to_string(), + dimension_y: "80".to_string(), + }); + + let my_model_clone = my_model.clone(); + let form = my_model.render_value(move |model| { + let on_submit = bind_rc!(my_model_clone, |new_model: MyModel| { + my_model_clone.set(new_model); + }); + + dom! { + + } + }); + + dom! { +
    +

    "Form 1:"

    + {form} +

    "Model 1:"

    +

    {my_model.map(|m| m.slug)} " / " {my_model.map(|m| m.name)}

    +

    {my_model.map(|m| m.dimension_x)} "x" {my_model.map(|m| m.dimension_y)}

    +
    + } +} diff --git a/storybook/src/form/form2.rs b/storybook/src/form/form2.rs new file mode 100644 index 0000000..c0874cc --- /dev/null +++ b/storybook/src/form/form2.rs @@ -0,0 +1,138 @@ +use std::rc::Rc; + +use vertigo::{bind_rc, component, css, dom, Value}; +use vertigo_forms::form::{DataSection, FormData, FormExport, FormParams, ModelForm}; + +// Form example 2 + +#[derive(Clone, PartialEq)] +pub enum Gender { + Male, + Female, +} + +impl std::fmt::Display for Gender { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Male => write!(f, "Male"), + Self::Female => write!(f, "Female"), + } + } +} + +impl From for Gender { + fn from(value: String) -> Self { + match value.as_str() { + "Female" => Self::Female, + _ => Self::Male, + } + } +} + +#[derive(Clone, PartialEq)] +pub struct MySecondModel { + pub first_name: String, + pub surname: String, + pub gender: Gender, + pub role: i64, + pub photo: String, +} + +impl From<&MySecondModel> for FormData { + fn from(value: &MySecondModel) -> Self { + let gender_map = vec!["Male".to_string(), "Female".to_string()]; + + let role_map = [ + (1i64, "Admin".to_string()), + (2, "Editor".to_string()), + (3, "Reporter".to_string()), + (4, "Viewer".to_string()), + ] + .into(); + + Self::default() + .with(DataSection::with_string_field( + "First Name", + "first_name", + &value.first_name, + )) + .with(DataSection::with_string_field( + "Surname", + "surname", + &value.surname, + )) + .with(DataSection::new("Gender").add_list_field( + "gender", + Some(value.gender.to_string()), + gender_map, + )) + .with(DataSection::new("Role").add_static_dict_field( + "role", + Some(value.role), + role_map, + )) + .with(DataSection::new("Photo").add_image_field("photo", Some(&value.photo))) + .add_bottom_controls() + } +} + +impl From for MySecondModel { + fn from(form_export: FormExport) -> Self { + Self { + first_name: form_export.get_string("first_name"), + surname: form_export.get_string("surname"), + gender: form_export.list("gender").unwrap_or(Gender::Male), + role: form_export.dict_or_default("role"), + photo: form_export.image_url("photo"), + } + } +} + +#[component] +pub fn Form2() { + let my_second_model: Value = Value::new(MySecondModel { + first_name: "Johann".to_string(), + surname: "Gambolputty".to_string(), + gender: Gender::Male, + role: 1, + photo: "https://picsum.photos/200".to_string(), + }); + + let my_model_clone = my_second_model.clone(); + let form = my_second_model.render_value(move |model| { + let on_submit = bind_rc!(my_model_clone, |new_model: MySecondModel| { + my_model_clone.set(new_model); + }); + + dom! { + + } + }); + + dom! { +
    +

    "Form 2:"

    + {form} +

    "Model 2:"

    +

    + {my_second_model.map(|m| m.first_name)} + " / " + {my_second_model.map(|m| m.surname)} + " (" {my_second_model.map(|m| m.gender.to_string())} ")" +

    +

    "Role: " {my_second_model.map(|m| m.role)}

    +

    + "Photo:"
    + +

    +
    + } +} diff --git a/storybook/src/form/mod.rs b/storybook/src/form/mod.rs new file mode 100644 index 0000000..7c5e487 --- /dev/null +++ b/storybook/src/form/mod.rs @@ -0,0 +1,44 @@ +use std::rc::Rc; +use vertigo::{dom, DomNode, Value}; +use vertigo_forms::{Tab, Tabs}; + +use crate::bordered_tabs; + +mod form1; + +mod form2; + +mod tabbed_form; + +pub fn form() -> DomNode { + type MyTabs = Tabs, T>; + dom! { + } + }), + }, + Tab { + key: "form2", + name: "Form 2".to_string(), + render: Rc::new(move |_| { + dom! { } + }), + }, + Tab { + key: "tabbed_form", + name: "Tabbed Form".to_string(), + render: Rc::new(move |_| { + dom! { } + }), + }, + ]} + params={bordered_tabs()} + /> + } +} diff --git a/storybook/src/form/tabbed_form.rs b/storybook/src/form/tabbed_form.rs new file mode 100644 index 0000000..5096c29 --- /dev/null +++ b/storybook/src/form/tabbed_form.rs @@ -0,0 +1,88 @@ +use std::rc::Rc; + +use vertigo::{bind_rc, component, css, dom, Value}; +use vertigo_forms::form::{ + DataFieldValue, DataSection, FormData, FormExport, FormParams, ModelForm, TextAreaValue, +}; + +use crate::bordered_tabs; + +// Tabbed Form example + +#[derive(Clone, PartialEq)] +pub struct TModel { + pub first_name: String, + pub last_name: String, + pub annotation: Option, +} + +impl From<&TModel> for FormData { + fn from(value: &TModel) -> Self { + Self::default() + .add_tab( + "Basic", + vec![ + DataSection::with_string_field("First name", "first_name", &value.first_name), + DataSection::with_string_field("Last name", "last_name", &value.last_name), + ], + ) + .add_tab( + "Other", + vec![DataSection::new("Annotation").add_field( + "annotation", + DataFieldValue::TextArea(TextAreaValue { + value: Value::new(value.annotation.clone().unwrap_or_default()), + original_value: value.annotation.clone().map(Rc::new), + rows: Some(10), + cols: None, + }), + )], + ) + .add_top_controls() + } +} + +impl From for TModel { + fn from(form_export: FormExport) -> Self { + Self { + first_name: form_export.get_string("first_name"), + last_name: form_export.get_string("last_name"), + annotation: form_export.get_string_opt("annotation"), + } + } +} + +#[component] +pub fn TabbedForm() { + let my_model: Value = Value::new(TModel { + first_name: "Johann".to_string(), + last_name: "Gambolputty".to_string(), + annotation: None, + }); + + let my_model_clone = my_model.clone(); + let form = my_model.render_value(move |model| { + let on_submit = bind_rc!(my_model_clone, |new_model: TModel| { + my_model_clone.set(new_model); + }); + + dom! { + + } + }); + + dom! { +
    +

    "Tabbed Form:"

    + {form} +
    + } +} diff --git a/storybook/src/input.rs b/storybook/src/input.rs index 33ba93f..71df725 100644 --- a/storybook/src/input.rs +++ b/storybook/src/input.rs @@ -33,7 +33,7 @@ pub fn input() -> DomNode { let list_input = dom! {

    "ListInput"

    - "Enter value: " + "Enter comma-separated values: "

    diff --git a/storybook/src/lib.rs b/storybook/src/lib.rs index 1957463..0fd1f42 100644 --- a/storybook/src/lib.rs +++ b/storybook/src/lib.rs @@ -163,28 +163,29 @@ fn render() -> DomNode { } } + +pub fn bordered_tabs() -> TabsParams { + TabsParams { + header_item_add_css: css! {" + border: 1px solid black; + padding: 0px 10px; + "}, + header_active_item_add_css: css! {" + background-color: lightgray; + "}, + content_css: css! {" + border: solid 1px black; + padding: 5px 10px; + "}, + container_css: css! {" + margin: 10px; + "}, + ..Default::default() + } +} diff --git a/storybook/src/tabs.rs b/storybook/src/tabs.rs index 23f2422..b75f020 100644 --- a/storybook/src/tabs.rs +++ b/storybook/src/tabs.rs @@ -3,6 +3,8 @@ use std::rc::Rc; use vertigo::{bind_rc, dom, DomNode, Value}; use vertigo_forms::{Tab, TabsContentMapped, TabsHeader}; +use crate::bordered_tabs; + #[derive(Clone, PartialEq)] pub enum MyView { View1, @@ -54,7 +56,7 @@ pub fn tabs() -> DomNode { DomNode { MyView::View2SubView1 | MyView::View2SubView2 => MyView::View2SubView1, } )} - params={} + params={bordered_tabs()} />

    }