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}