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: <select> fuers
Dropdown, <input type=checkbox|radio> 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.
This commit is contained in:
2026-08-27 14:01:12 +02:00
parent fe1d736a2d
commit 0379969458
16 changed files with 212 additions and 0 deletions

View File

@@ -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 }
}
}
}

View File

@@ -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<MouseEvent>,
}

View File

@@ -0,0 +1,2 @@
pub mod custom_button_component;
pub mod custom_button_props;

View File

@@ -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}"
}
}
}
}
}

View File

@@ -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<String>,
#[props(into)]
pub selected: String,
#[props(into)]
pub css: String,
pub onchange: Callback<String>,
}

View File

@@ -0,0 +1,2 @@
pub mod custom_dropdown_component;
pub mod custom_dropdown_props;

View File

@@ -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()),
}
}
}

View File

@@ -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<String>,
}

View File

@@ -0,0 +1,2 @@
pub mod custom_input_component;
pub mod custom_input_props;

View File

@@ -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<String> {
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<String> = 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"]);
}
}

View File

@@ -0,0 +1,11 @@
use dioxus::prelude::*;
#[derive(Props, Clone, PartialEq)]
pub struct CustomMultiSelectProps {
pub options: Vec<String>,
pub selected: Vec<String>,
#[props(into)]
pub css: String,
/// Bekommt die komplette neue Auswahl, nicht nur die angeklickte Option.
pub onchange: Callback<Vec<String>>,
}

View File

@@ -0,0 +1,2 @@
pub mod custom_multi_select_component;
pub mod custom_multi_select_props;

View File

@@ -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}"
}
}
}
}
}

View File

@@ -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<String>,
#[props(into)]
pub selected: String,
#[props(into)]
pub css: String,
pub onchange: Callback<String>,
}

View File

@@ -0,0 +1,2 @@
pub mod custom_single_select_component;
pub mod custom_single_select_props;

View File

@@ -1 +1,6 @@
pub mod custom_button;
pub mod custom_dropdown;
pub mod custom_input;
pub mod custom_link; pub mod custom_link;
pub mod custom_multi_select;
pub mod custom_single_select;