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:
13
src/components/custom_button/custom_button_component.rs
Normal file
13
src/components/custom_button/custom_button_component.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/components/custom_button/custom_button_props.rs
Normal file
10
src/components/custom_button/custom_button_props.rs
Normal 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>,
|
||||||
|
}
|
||||||
2
src/components/custom_button/mod.rs
Normal file
2
src/components/custom_button/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_button_component;
|
||||||
|
pub mod custom_button_props;
|
||||||
22
src/components/custom_dropdown/custom_dropdown_component.rs
Normal file
22
src/components/custom_dropdown/custom_dropdown_component.rs
Normal 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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/components/custom_dropdown/custom_dropdown_props.rs
Normal file
13
src/components/custom_dropdown/custom_dropdown_props.rs
Normal 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>,
|
||||||
|
}
|
||||||
2
src/components/custom_dropdown/mod.rs
Normal file
2
src/components/custom_dropdown/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_dropdown_component;
|
||||||
|
pub mod custom_dropdown_props;
|
||||||
16
src/components/custom_input/custom_input_component.rs
Normal file
16
src/components/custom_input/custom_input_component.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/components/custom_input/custom_input_props.rs
Normal file
13
src/components/custom_input/custom_input_props.rs
Normal 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>,
|
||||||
|
}
|
||||||
2
src/components/custom_input/mod.rs
Normal file
2
src/components/custom_input/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_input_component;
|
||||||
|
pub mod custom_input_props;
|
||||||
@@ -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"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>>,
|
||||||
|
}
|
||||||
2
src/components/custom_multi_select/mod.rs
Normal file
2
src/components/custom_multi_select/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_multi_select_component;
|
||||||
|
pub mod custom_multi_select_props;
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
}
|
||||||
2
src/components/custom_single_select/mod.rs
Normal file
2
src/components/custom_single_select/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_single_select_component;
|
||||||
|
pub mod custom_single_select_props;
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user