slack_blocks/elems/
text_input.rs

1//! # Plain Text Input
2//!
3//! [slack api docs 🔗]
4//!
5//! A plain-text input, similar to the HTML `<input>` tag, creates a field where a user can enter freeform data.
6//! It can appear as a single-line field or a larger textarea using the `multiline` flag.
7//!
8//! Works in [blocks 🔗]: Input
9//! Works in [app surfaces 🔗]: Home tabs, Modals, Messages
10//!
11//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
12
13use std::borrow::Cow;
14
15use serde::{Deserialize as De, Serialize as Ser};
16#[cfg(feature = "validation")]
17use validator::Validate;
18
19use crate::text;
20#[cfg(feature = "validation")]
21use crate::val_helpr::*;
22
23/// Interaction types that you would like to receive a [`block_actions` payload 🔗] for.
24///
25/// [`block_actions` payload 🔗]: https://api.slack.com/reference/interaction-payloads/block-actions
26#[derive(Clone, Copy, Debug, Hash, PartialEq, Ser, De)]
27#[serde(untagged, rename_all = "snake_case")]
28pub enum ActionTrigger {
29  /// Payload is dispatched when user presses the enter key while the input is in focus.
30  ///
31  /// Hint text will appear underneath the input explaining to the user to press enter to submit.
32  OnEnterPressed,
33
34  /// Payload is dispatched when a character is entered (or removed) in the input.
35  OnCharacterEntered,
36}
37
38/// [api docs](https://api.slack.com/reference/block-kit/composition-objects#dispatch_action_config)
39#[derive(Clone, Debug, Hash, PartialEq, Ser, De)]
40struct DispatchActionConfig {
41  trigger_actions_on: Vec<ActionTrigger>,
42}
43
44/// # Plain Text Input
45///
46/// [slack api docs 🔗]
47///
48/// A plain-text input, similar to the HTML `<input>` tag, creates a field where a user can enter freeform data.
49/// It can appear as a single-line field or a larger textarea using the `multiline` flag.
50///
51/// Works in [blocks 🔗]: Input
52/// Works in [app surfaces 🔗]: Home tabs, Modals, Messages
53///
54/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
55#[derive(Clone, Debug, Hash, PartialEq, Ser, De)]
56#[cfg_attr(feature = "validation", derive(Validate))]
57pub struct TextInput<'a> {
58  #[cfg_attr(feature = "validation", validate(length(max = 255)))]
59  action_id: Cow<'a, str>,
60
61  #[cfg_attr(feature = "validation",
62             validate(custom = "validate_placeholder"))]
63  #[serde(skip_serializing_if = "Option::is_none")]
64  placeholder: Option<text::Text>,
65
66  #[serde(skip_serializing_if = "Option::is_none")]
67  initial_value: Option<Cow<'a, str>>,
68
69  #[serde(skip_serializing_if = "Option::is_none")]
70  multiline: Option<bool>,
71
72  #[cfg_attr(feature = "validation", validate(range(max = 3000)))]
73  #[serde(skip_serializing_if = "Option::is_none")]
74  min_length: Option<u32>,
75
76  #[serde(skip_serializing_if = "Option::is_none")]
77  max_length: Option<u32>,
78
79  #[serde(skip_serializing_if = "Option::is_none")]
80  dispatch_action_config: Option<DispatchActionConfig>,
81}
82
83impl<'a> TextInput<'a> {
84  /// Build a new text input block element
85  ///
86  /// # Examples
87  /// See example for `build::TextInputBuilder`.
88  pub fn builder() -> build::TextInputBuilderInit<'a> {
89    build::TextInputBuilderInit::new()
90  }
91
92  /// Validate that this select element agrees with Slack's model requirements
93  ///
94  /// # Errors
95  /// - length of `placeholder` greater than 150
96  /// - length of `action_id` greater than 255
97  /// - value of `min_length` greater than 3000
98  ///
99  /// # Example
100  /// ```
101  /// use slack_blocks::elems::TextInput;
102  ///
103  /// let long_string = || std::iter::repeat('a').take(256).collect::<String>();
104  ///
105  /// let input = TextInput::builder().action_id(long_string())
106  ///                                 .placeholder(long_string())
107  ///                                 .min_length(3001)
108  ///                                 .build();
109  ///
110  /// assert!(matches!(input.validate(), Err(_)))
111  /// ```
112  #[cfg(feature = "validation")]
113  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
114  pub fn validate(&self) -> ValidationResult {
115    Validate::validate(self)
116  }
117}
118
119#[cfg(feature = "validation")]
120fn validate_placeholder<'a>(p: &text::Text) -> ValidatorResult {
121  below_len("TextInput.placeholder", 150, p)
122}
123
124/// Text Input Builder
125pub mod build {
126  use std::marker::PhantomData;
127
128  use super::*;
129  use crate::build::*;
130
131  /// Required Builder Method markers
132  #[allow(non_camel_case_types)]
133  pub mod method {
134    /// TextInputBuilder.action_id
135    #[derive(Copy, Clone, Debug)]
136    pub struct action_id;
137  }
138
139  /// Initial state for TextInputBuilder
140  pub type TextInputBuilderInit<'a> =
141    TextInputBuilder<'a, RequiredMethodNotCalled<method::action_id>>;
142
143  /// Build a Text Input element
144  ///
145  /// Allows you to construct a text input safely, with compile-time checks
146  /// on required setter methods.
147  ///
148  /// # Required Methods
149  /// `TextInputBuilder::build()` is only available if these methods have been called:
150  ///  - `action_id`
151  ///
152  /// # Examples
153  ///
154  /// ```
155  /// use slack_blocks::{blocks::{Block, Input},
156  ///                    elems::TextInput};
157  ///
158  /// let text_input = TextInput::builder().action_id("plate_num")
159  ///                                      .placeholder("ABC1234")
160  ///                                      .length(1..=7)
161  ///                                      .build();
162  ///
163  /// let block: Block = Input::builder().label("enter custom license plate")
164  ///                                    .element(text_input)
165  ///                                    .dispatch_actions(true)
166  ///                                    .build()
167  ///                                    .into();
168  /// ```
169  #[derive(Debug)]
170  pub struct TextInputBuilder<'a, A> {
171    action_id: Option<Cow<'a, str>>,
172    placeholder: Option<text::Text>,
173    initial_value: Option<Cow<'a, str>>,
174    multiline: Option<bool>,
175    min_length: Option<u32>,
176    max_length: Option<u32>,
177    dispatch_action_config: Option<DispatchActionConfig>,
178    state: PhantomData<A>,
179  }
180
181  impl<'a, A> TextInputBuilder<'a, A> {
182    /// Construct a new text input builder of empty state
183    pub fn new() -> Self {
184      Self { action_id: None,
185             placeholder: None,
186             initial_value: None,
187             multiline: None,
188             min_length: None,
189             max_length: None,
190             dispatch_action_config: None,
191             state: PhantomData::<_> }
192    }
193
194    /// Set `action_id` (**Required**)
195    ///
196    /// An identifier for the input value when the parent modal is submitted.
197    ///
198    /// You can use this when you receive a `view_submission` payload [to identify the value of the input element 🔗].
199    ///
200    /// Should be unique among all other `action_id`s in the containing block.
201    ///
202    /// Maximum length for this field is 255 characters.
203    ///
204    /// [to identify the value of the input element 🔗]: https://api.slack.com/surfaces/modals/using#handling-submissions
205    pub fn action_id(self,
206                     action_id: impl Into<Cow<'a, str>>)
207                     -> TextInputBuilder<'a, Set<method::action_id>> {
208      TextInputBuilder { action_id: Some(action_id.into()),
209                         placeholder: self.placeholder,
210                         initial_value: self.initial_value,
211                         multiline: self.multiline,
212                         min_length: self.min_length,
213                         max_length: self.max_length,
214                         dispatch_action_config: self.dispatch_action_config,
215                         state: PhantomData::<_> }
216    }
217
218    /// Add a new event trigger (Optional)
219    ///
220    /// In messages, in order to receive events you must invoke this method and set `dispatch_action` to `true` on the containing Input block.
221    ///
222    /// In modals and other contexts, the value of this element will be included with the submission of the form.
223    ///
224    /// By invoking this with `ActionTrigger::OnCharacterEntered`, `ActionTrigger::OnEnterPressed`, or both,
225    /// you can configure the input element to send additional events when these triggers are fired by the client.
226    ///
227    /// For more info on these events, see [`block_actions` interaction payload 🔗].
228    ///
229    /// [`block_actions` interaction payload 🔗]: https://api.slack.com/reference/interaction-payloads/block-actions
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use slack_blocks::{blocks::{Block, Input},
235    ///                    elems::{text_input::ActionTrigger::OnCharacterEntered,
236    ///                            TextInput}};
237    ///
238    /// let text_input = TextInput::builder().action_id("plate_num")
239    ///                                      .placeholder("ABC1234")
240    ///                                      .length(1..=7)
241    ///                                      .action_trigger(OnCharacterEntered)
242    ///                                      .build();
243    ///
244    /// let block: Block = Input::builder().label("enter custom license plate")
245    ///                                    .element(text_input)
246    ///                                    .dispatch_actions(true)
247    ///                                    .build()
248    ///                                    .into();
249    /// ```
250    pub fn action_trigger(mut self, trigger: ActionTrigger) -> Self {
251      let config =
252        self.dispatch_action_config
253            .map(|mut c| {
254              if !c.trigger_actions_on.contains(&trigger) {
255                c.trigger_actions_on.push(trigger)
256              }
257
258              c
259            })
260            .unwrap_or_else(|| DispatchActionConfig { trigger_actions_on:
261                                                        vec![trigger] });
262
263      self.dispatch_action_config = Some(config);
264      self
265    }
266
267    /// Set `placeholder` (**Optional**)
268    ///
269    /// A [`plain_text` only text object 🔗] that defines the placeholder text shown in the plain-text input.
270    ///
271    /// Maximum length for the `text` in this field is 150 characters.
272    ///
273    /// [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
274    pub fn placeholder(mut self, placeholder: impl Into<text::Plain>) -> Self {
275      self.placeholder = Some(placeholder.into().into());
276      self
277    }
278
279    /// Set `initial value` (**Optional**)
280    ///
281    /// The initial value in the plain-text input when it is loaded.
282    pub fn initial_value(mut self, init: impl Into<Cow<'a, str>>) -> Self {
283      self.initial_value = Some(init.into());
284      self
285    }
286
287    /// Set `multiline` (**Optional**)
288    ///
289    /// Indicates that the input will be a larger textarea,
290    /// rather than a single line.
291    ///
292    /// Default is `false`.
293    pub fn multiline(mut self, multiline: bool) -> Self {
294      self.multiline = Some(multiline);
295      self
296    }
297
298    /// Set `min_length` (**Optional**)
299    ///
300    /// The minimum length of input that the user must provide.
301    ///
302    /// If the user provides less, they will receive an error.
303    ///
304    /// Maximum value is 3000.
305    pub fn min_length(mut self, min: u32) -> Self {
306      self.min_length = Some(min.into());
307      self
308    }
309
310    /// Set `max_length` (**Optional**)
311    ///
312    /// The maximum length of input that the user can provide.
313    ///
314    /// If the user provides more, they will receive an error.
315    pub fn max_length(mut self, max: u32) -> Self {
316      self.max_length = Some(max.into());
317      self
318    }
319
320    /// Set `min_length` and/or `max_length` with a rust range literal (**Optional**)
321    ///
322    /// ```
323    /// use slack_blocks::elems::TextInput;
324    ///
325    /// TextInput::builder().action_id("vanity_plate")
326    ///                     .placeholder("enter your desired custom license plate")
327    ///                     .length(1..=7);
328    /// ```
329    ///
330    /// ```
331    /// use slack_blocks::elems::TextInput;
332    ///
333    /// TextInput::builder().action_id("first_name")
334    ///                     .placeholder("enter your first name")
335    ///                     .length(2..);
336    /// ```
337    ///
338    /// ```
339    /// use slack_blocks::elems::TextInput;
340    ///
341    /// TextInput::builder()
342    ///           .action_id("does nothing")
343    ///           .placeholder("This is the same as not calling length at all!")
344    ///           .length(..);
345    /// ```
346    pub fn length(mut self, rng: impl std::ops::RangeBounds<u32>) -> Self {
347      use std::ops::Bound;
348
349      self.min_length = match rng.start_bound() {
350        | Bound::Included(min) => Some(*min),
351        | Bound::Excluded(min) => Some(min + 1),
352        | Bound::Unbounded => None,
353      };
354
355      self.max_length = match rng.end_bound() {
356        | Bound::Included(max) => Some(*max),
357        | Bound::Excluded(max) => Some(max - 1),
358        | Bound::Unbounded => None,
359      };
360
361      self
362    }
363  }
364
365  impl<'a> TextInputBuilder<'a, Set<method::action_id>> {
366    /// All done building, now give me a darn text input!
367    ///
368    /// > `no method name 'build' found for struct 'text_input::build::TextInputBuilder<...>'`?
369    /// Make sure all required setter methods have been called. See docs for `TextInputBuilder`.
370    ///
371    /// ```compile_fail
372    /// use slack_blocks::elems::TextInput;
373    ///
374    /// let sel = TextInput::builder().build(); // Won't compile!
375    /// ```
376    ///
377    /// ```
378    /// use slack_blocks::elems::TextInput;
379    ///
380    /// let sel = TextInput::builder().action_id("bar").build();
381    /// ```
382    pub fn build(self) -> TextInput<'a> {
383      TextInput { action_id: self.action_id.unwrap(),
384                  placeholder: self.placeholder,
385                  initial_value: self.initial_value,
386                  multiline: self.multiline,
387                  min_length: self.min_length,
388                  max_length: self.max_length,
389                  dispatch_action_config: self.dispatch_action_config }
390    }
391  }
392}