Skip to main content

patternfly_yew/components/
number_input.rs

1use std::{fmt::Display, str::FromStr};
2
3use crate::prelude::*;
4use num_traits::PrimInt;
5use yew::prelude::*;
6
7/// Position of the number input unit in relation to the number input.
8#[derive(Debug, Clone, PartialEq)]
9pub enum NumberInputUnit {
10    Before(Html),
11    After(Html),
12}
13
14#[derive(Debug, Clone, PartialEq, Properties)]
15pub struct NumberInputProperties<T: PrimInt + Display + FromStr + 'static> {
16    /// Value of the number input.
17    #[prop_or(T::zero())]
18    pub value: T,
19    /// Additional classes added to the number input.
20    #[prop_or_default]
21    pub class: Classes,
22    /// Sets the width of the number input to a number of characters
23    #[prop_or_default]
24    pub width_chars: Option<u8>,
25    /// Indicates whether the whole number input should be disabled.
26    #[prop_or_default]
27    pub disabled: bool,
28    /// Callback for the minus button.
29    #[prop_or_default]
30    pub onminus: Option<Callback<()>>,
31    /// Callback the text input changing.
32    #[prop_or_default]
33    pub onchange: Option<Callback<T>>,
34    /// Callback for the plus button.
35    #[prop_or_default]
36    pub onplus: Option<Callback<()>>,
37    /// Adds the given unit to the number input.
38    #[prop_or_default]
39    pub unit: Option<NumberInputUnit>,
40    /// Minimum value of the number input, disabling the minus button when reached.
41    #[prop_or(T::min_value())]
42    pub min: T,
43    /// Maximum value of the number input, disabling the plus button when reached.
44    #[prop_or(T::max_value())]
45    pub max: T,
46    /// Value to indicate if the input is modified to show the validiation state.
47    #[prop_or_default]
48    pub state: InputState,
49    /// Name of the input.
50    #[prop_or_default]
51    pub input_name: Option<String>,
52    /// Aria label of the minus button.
53    #[prop_or(AttrValue::from("Minus"))]
54    pub minus_button_aria_label: AttrValue,
55    /// Aria label of the plus button.
56    #[prop_or(AttrValue::from("Plus"))]
57    pub plus_button_aria_label: AttrValue,
58}
59
60#[function_component(NumberInput)]
61pub fn number_input<T: PrimInt + Display + FromStr + 'static>(
62    props: &NumberInputProperties<T>,
63) -> Html {
64    let mut class = props.class.clone();
65    class.push("pf-v6-c-number-input");
66    if props.state != InputState::Default {
67        class.push("pf-m-status");
68    }
69    let width_style_name = "--pf-v6-c-number-input--c-form-control--width-chars";
70    let style = props
71        .width_chars
72        .map(|w| format!("{width_style_name}:{w};"));
73
74    let onminusclick = use_callback(props.onminus.clone(), |_, onminus| {
75        if let Some(onminus) = onminus {
76            onminus.emit(());
77        }
78    });
79    let onplusclick = use_callback(props.onplus.clone(), |_, onplus| {
80        if let Some(onplus) = onplus {
81            onplus.emit(());
82        }
83    });
84    let onchange = use_callback(props.onchange.clone(), |new_val: String, onchange| {
85        let Some(onchange) = onchange else {
86            return;
87        };
88        match new_val.parse::<T>() {
89            Ok(n) => onchange.emit(n),
90            Err(_) => log::warn!("[NumberInput] Failed to parse {new_val} into a number."),
91        };
92    });
93    html! {
94        <div {class} {style}>
95            if let Some(NumberInputUnit::Before(unit)) = &props.unit {
96                <Unit>{ unit.clone() }</Unit>
97            }
98            <InputGroup>
99                <InputGroupItem>
100                    <Button
101                        variant={ButtonVariant::Control}
102                        aria_label={props.minus_button_aria_label.clone()}
103                        disabled={props.disabled || props.value <= props.min}
104                        onclick={onminusclick}
105                    >
106                        <span class="pf-v6-c-number-input__icon">{ Icon::Minus }</span>
107                    </Button>
108                </InputGroupItem>
109                <InputGroupItem>
110                    <TextInput
111                        r#type={TextInputType::Number}
112                        value={props.value.to_string()}
113                        name={props.input_name.clone()}
114                        disabled={props.disabled}
115                        onchange={onchange}
116                        state={props.state}
117                    />
118                </InputGroupItem>
119                <InputGroupItem>
120                    <Button
121                        variant={ButtonVariant::Control}
122                        aria_label={props.plus_button_aria_label.clone()}
123                        disabled={props.disabled || props.value >= props.max}
124                        onclick={onplusclick}
125                    >
126                        <span class="pf-v6-c-number-input__icon">{ Icon::Plus }</span>
127                    </Button>
128                </InputGroupItem>
129            </InputGroup>
130            if let Some(NumberInputUnit::After(unit)) = &props.unit {
131                <Unit>{ unit.clone() }</Unit>
132            }
133        </div>
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Properties)]
138struct UnitProperties {
139    children: Html,
140}
141
142#[function_component(Unit)]
143fn unit(props: &UnitProperties) -> Html {
144    html!(<div class="pf-v6-c-number-input__unit">{ props.children.clone() }</div>)
145}