From 0379969458b95117f86013425929f534dbec99a7 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Thu, 27 Aug 2026 14:01:12 +0200 Subject: [PATCH] feat(components): Button, Input, Dropdown, Multi- und Single-Select ergaenzen Fuenf Formular-Komponenten nach dem Muster von CustomLink: eigene Props-Struct plus Komponentenfunktion, controlled - der Zustand bleibt beim Aufrufer, die Komponente meldet Aenderungen per Callback. Bewusst native HTML-Elemente statt eigener Popups und Listen: fuer Multi- und Single-Select. Damit kommen Tastaturbedienung, Screenreader-Support und die Auswahldialoge auf Mobilgeraeten ohne eigenen Code. CustomMultiSelect liefert dem Callback die vollstaendige neue Auswahl statt nur der angeklickten Option, sonst muesste jede Aufrufstelle die Umschaltlogik nachbauen. Diese Logik steckt in toggled() und ist getestet. --- .../custom_button/custom_button_component.rs | 13 +++++ .../custom_button/custom_button_props.rs | 10 ++++ src/components/custom_button/mod.rs | 2 + .../custom_dropdown_component.rs | 22 ++++++++ .../custom_dropdown/custom_dropdown_props.rs | 13 +++++ src/components/custom_dropdown/mod.rs | 2 + .../custom_input/custom_input_component.rs | 16 ++++++ .../custom_input/custom_input_props.rs | 13 +++++ src/components/custom_input/mod.rs | 2 + .../custom_multi_select_component.rs | 55 +++++++++++++++++++ .../custom_multi_select_props.rs | 11 ++++ src/components/custom_multi_select/mod.rs | 2 + .../custom_single_select_component.rs | 29 ++++++++++ .../custom_single_select_props.rs | 15 +++++ src/components/custom_single_select/mod.rs | 2 + src/components/mod.rs | 5 ++ 16 files changed, 212 insertions(+) create mode 100644 src/components/custom_button/custom_button_component.rs create mode 100644 src/components/custom_button/custom_button_props.rs create mode 100644 src/components/custom_button/mod.rs create mode 100644 src/components/custom_dropdown/custom_dropdown_component.rs create mode 100644 src/components/custom_dropdown/custom_dropdown_props.rs create mode 100644 src/components/custom_dropdown/mod.rs create mode 100644 src/components/custom_input/custom_input_component.rs create mode 100644 src/components/custom_input/custom_input_props.rs create mode 100644 src/components/custom_input/mod.rs create mode 100644 src/components/custom_multi_select/custom_multi_select_component.rs create mode 100644 src/components/custom_multi_select/custom_multi_select_props.rs create mode 100644 src/components/custom_multi_select/mod.rs create mode 100644 src/components/custom_single_select/custom_single_select_component.rs create mode 100644 src/components/custom_single_select/custom_single_select_props.rs create mode 100644 src/components/custom_single_select/mod.rs diff --git a/src/components/custom_button/custom_button_component.rs b/src/components/custom_button/custom_button_component.rs new file mode 100644 index 0000000..edf86a6 --- /dev/null +++ b/src/components/custom_button/custom_button_component.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; +use crate::components::custom_button::custom_button_props::CustomButtonProps; + +#[allow(non_snake_case)] +pub fn CustomButton(props: CustomButtonProps) -> Element { + rsx! { + button { + class: "{props.css}", + onclick: props.onclick, + { props.label } + } + } +} diff --git a/src/components/custom_button/custom_button_props.rs b/src/components/custom_button/custom_button_props.rs new file mode 100644 index 0000000..226b824 --- /dev/null +++ b/src/components/custom_button/custom_button_props.rs @@ -0,0 +1,10 @@ +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct CustomButtonProps { + #[props(into)] + pub label: String, + #[props(into)] + pub css: String, + pub onclick: Callback, +} diff --git a/src/components/custom_button/mod.rs b/src/components/custom_button/mod.rs new file mode 100644 index 0000000..4975a68 --- /dev/null +++ b/src/components/custom_button/mod.rs @@ -0,0 +1,2 @@ +pub mod custom_button_component; +pub mod custom_button_props; diff --git a/src/components/custom_dropdown/custom_dropdown_component.rs b/src/components/custom_dropdown/custom_dropdown_component.rs new file mode 100644 index 0000000..038ec91 --- /dev/null +++ b/src/components/custom_dropdown/custom_dropdown_component.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; +use crate::components::custom_dropdown::custom_dropdown_props::CustomDropdownProps; + +#[allow(non_snake_case)] +pub fn CustomDropdown(props: CustomDropdownProps) -> Element { + let onchange = props.onchange; + + rsx! { + select { + class: "{props.css}", + onchange: move |event| onchange.call(event.value()), + for opt in props.options.clone() { + option { + key: "{opt}", + value: "{opt}", + selected: opt == props.selected, + "{opt}" + } + } + } + } +} diff --git a/src/components/custom_dropdown/custom_dropdown_props.rs b/src/components/custom_dropdown/custom_dropdown_props.rs new file mode 100644 index 0000000..c9f35b8 --- /dev/null +++ b/src/components/custom_dropdown/custom_dropdown_props.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct CustomDropdownProps { + // ponytail: Option-Label == Option-Wert. Wenn beides mal auseinanderfallen muss, + // wird daraus Vec<(String, String)> — betrifft nur diese Datei und die Call-Sites. + pub options: Vec, + #[props(into)] + pub selected: String, + #[props(into)] + pub css: String, + pub onchange: Callback, +} diff --git a/src/components/custom_dropdown/mod.rs b/src/components/custom_dropdown/mod.rs new file mode 100644 index 0000000..a0dc205 --- /dev/null +++ b/src/components/custom_dropdown/mod.rs @@ -0,0 +1,2 @@ +pub mod custom_dropdown_component; +pub mod custom_dropdown_props; diff --git a/src/components/custom_input/custom_input_component.rs b/src/components/custom_input/custom_input_component.rs new file mode 100644 index 0000000..1d9b567 --- /dev/null +++ b/src/components/custom_input/custom_input_component.rs @@ -0,0 +1,16 @@ +use dioxus::prelude::*; +use crate::components::custom_input::custom_input_props::CustomInputProps; + +#[allow(non_snake_case)] +pub fn CustomInput(props: CustomInputProps) -> Element { + let oninput = props.oninput; + + rsx! { + input { + class: "{props.css}", + value: "{props.value}", + placeholder: "{props.placeholder}", + oninput: move |event| oninput.call(event.value()), + } + } +} diff --git a/src/components/custom_input/custom_input_props.rs b/src/components/custom_input/custom_input_props.rs new file mode 100644 index 0000000..423b71a --- /dev/null +++ b/src/components/custom_input/custom_input_props.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct CustomInputProps { + /// Aktueller Wert — die Komponente ist controlled, der Zustand liegt beim Aufrufer. + #[props(into)] + pub value: String, + #[props(into, default)] + pub placeholder: String, + #[props(into)] + pub css: String, + pub oninput: Callback, +} diff --git a/src/components/custom_input/mod.rs b/src/components/custom_input/mod.rs new file mode 100644 index 0000000..7f1a7cb --- /dev/null +++ b/src/components/custom_input/mod.rs @@ -0,0 +1,2 @@ +pub mod custom_input_component; +pub mod custom_input_props; diff --git a/src/components/custom_multi_select/custom_multi_select_component.rs b/src/components/custom_multi_select/custom_multi_select_component.rs new file mode 100644 index 0000000..8b86a8c --- /dev/null +++ b/src/components/custom_multi_select/custom_multi_select_component.rs @@ -0,0 +1,55 @@ +use dioxus::prelude::*; +use crate::components::custom_multi_select::custom_multi_select_props::CustomMultiSelectProps; + +#[allow(non_snake_case)] +pub fn CustomMultiSelect(props: CustomMultiSelectProps) -> Element { + let onchange = props.onchange; + let selected = props.selected.clone(); + + rsx! { + div { + class: "{props.css}", + for opt in props.options.clone() { + label { + key: "{opt}", + input { + r#type: "checkbox", + checked: selected.contains(&opt), + onchange: { + let selected = selected.clone(); + let opt = opt.clone(); + move |_| onchange.call(toggled(&selected, &opt)) + }, + } + "{opt}" + } + } + } + } +} + +/// Auswahl mit `option` umschalten. Reihenfolge der übrigen Einträge bleibt erhalten. +fn toggled(selected: &[String], option: &str) -> Vec { + let mut next = selected.to_vec(); + match next.iter().position(|entry| entry == option) { + Some(index) => { + next.remove(index); + } + None => next.push(option.to_string()), + } + next +} + +#[cfg(test)] +mod tests { + use super::toggled; + + #[test] + fn toggles_in_and_out_without_reordering() { + let selected: Vec = vec!["a".into(), "b".into()]; + + assert_eq!(toggled(&selected, "c"), vec!["a", "b", "c"]); + assert_eq!(toggled(&selected, "a"), vec!["b"]); + assert_eq!(toggled(&toggled(&selected, "a"), "a"), vec!["b", "a"]); + } +} diff --git a/src/components/custom_multi_select/custom_multi_select_props.rs b/src/components/custom_multi_select/custom_multi_select_props.rs new file mode 100644 index 0000000..4d857f8 --- /dev/null +++ b/src/components/custom_multi_select/custom_multi_select_props.rs @@ -0,0 +1,11 @@ +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct CustomMultiSelectProps { + pub options: Vec, + pub selected: Vec, + #[props(into)] + pub css: String, + /// Bekommt die komplette neue Auswahl, nicht nur die angeklickte Option. + pub onchange: Callback>, +} diff --git a/src/components/custom_multi_select/mod.rs b/src/components/custom_multi_select/mod.rs new file mode 100644 index 0000000..c918832 --- /dev/null +++ b/src/components/custom_multi_select/mod.rs @@ -0,0 +1,2 @@ +pub mod custom_multi_select_component; +pub mod custom_multi_select_props; diff --git a/src/components/custom_single_select/custom_single_select_component.rs b/src/components/custom_single_select/custom_single_select_component.rs new file mode 100644 index 0000000..53ab5d5 --- /dev/null +++ b/src/components/custom_single_select/custom_single_select_component.rs @@ -0,0 +1,29 @@ +use dioxus::prelude::*; +use crate::components::custom_single_select::custom_single_select_props::CustomSingleSelectProps; + +#[allow(non_snake_case)] +pub fn CustomSingleSelect(props: CustomSingleSelectProps) -> Element { + let onchange = props.onchange; + + rsx! { + div { + class: "{props.css}", + for opt in props.options.clone() { + label { + key: "{opt}", + input { + r#type: "radio", + name: "{props.name}", + value: "{opt}", + checked: opt == props.selected, + onchange: { + let opt = opt.clone(); + move |_| onchange.call(opt.clone()) + }, + } + "{opt}" + } + } + } + } +} diff --git a/src/components/custom_single_select/custom_single_select_props.rs b/src/components/custom_single_select/custom_single_select_props.rs new file mode 100644 index 0000000..1144373 --- /dev/null +++ b/src/components/custom_single_select/custom_single_select_props.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; + +#[derive(Props, Clone, PartialEq)] +pub struct CustomSingleSelectProps { + /// Gruppenname der Radio-Buttons — muss pro Gruppe eindeutig sein, + /// sonst schalten sich zwei Gruppen gegenseitig ab. + #[props(into)] + pub name: String, + pub options: Vec, + #[props(into)] + pub selected: String, + #[props(into)] + pub css: String, + pub onchange: Callback, +} diff --git a/src/components/custom_single_select/mod.rs b/src/components/custom_single_select/mod.rs new file mode 100644 index 0000000..5712ccd --- /dev/null +++ b/src/components/custom_single_select/mod.rs @@ -0,0 +1,2 @@ +pub mod custom_single_select_component; +pub mod custom_single_select_props; diff --git a/src/components/mod.rs b/src/components/mod.rs index c73b5d6..49f973d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1 +1,6 @@ +pub mod custom_button; +pub mod custom_dropdown; +pub mod custom_input; pub mod custom_link; +pub mod custom_multi_select; +pub mod custom_single_select;