This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/html/
classes.rs

1use std::borrow::Cow;
2use std::iter::FromIterator;
3use std::rc::Rc;
4
5use implicit_clone::ImplicitClone;
6use indexmap::IndexSet;
7
8use super::IntoPropValue;
9use crate::utils::RcExt;
10use crate::virtual_dom::AttrValue;
11
12/// A set of classes, cheap to clone.
13///
14/// The preferred way of creating this is using the [`classes!`][yew::classes!] macro.
15#[derive(Debug, Clone, Default)]
16pub struct Classes {
17    set: Rc<IndexSet<AttrValue>>,
18}
19
20impl ImplicitClone for Classes {}
21
22/// helper method to efficiently turn a set of classes into a space-separated
23/// string. Abstracts differences between ToString and IntoPropValue. The
24/// `rest` iterator is cloned to pre-compute the length of the String; it
25/// should be cheap to clone.
26fn build_attr_value(first: AttrValue, rest: impl Iterator<Item = AttrValue> + Clone) -> AttrValue {
27    // The length of the string is known to be the length of all the
28    // components, plus one space for each element in `rest`.
29    let mut s = String::with_capacity(
30        rest.clone()
31            .map(|class| class.len())
32            .chain([first.len(), rest.size_hint().0])
33            .sum(),
34    );
35
36    s.push_str(first.as_str());
37    // NOTE: this can be improved once Iterator::intersperse() becomes stable
38    for class in rest {
39        s.push(' ');
40        s.push_str(class.as_str());
41    }
42    s.into()
43}
44
45impl Classes {
46    /// Creates an empty set of classes. (Does not allocate.)
47    #[inline]
48    pub fn new() -> Self {
49        Self {
50            set: Rc::new(IndexSet::new()),
51        }
52    }
53
54    /// Creates an empty set of classes with capacity for n elements. (Does not allocate if n is
55    /// zero.)
56    #[inline]
57    pub fn with_capacity(n: usize) -> Self {
58        Self {
59            set: Rc::new(IndexSet::with_capacity(n)),
60        }
61    }
62
63    /// Adds a class to a set.
64    ///
65    /// If the provided class has already been added, this method will ignore it.
66    pub fn push<T: Into<Self>>(&mut self, class: T) {
67        let classes_to_add: Self = class.into();
68        if self.is_empty() {
69            *self = classes_to_add
70        } else {
71            Rc::make_mut(&mut self.set).extend(classes_to_add.set.iter().cloned())
72        }
73    }
74
75    /// Adds a class to a set.
76    ///
77    /// If the provided class has already been added, this method will ignore it.
78    ///
79    /// This method won't check if there are multiple classes in the input string.
80    ///
81    /// # Safety
82    ///
83    /// This function will not split the string into multiple classes. Please do not use it unless
84    /// you are absolutely certain that the string does not contain any whitespace and it is not
85    /// empty. Using `push()`  is preferred.
86    pub unsafe fn unchecked_push<T: Into<AttrValue>>(&mut self, class: T) {
87        Rc::make_mut(&mut self.set).insert(class.into());
88    }
89
90    /// Check the set contains a class.
91    #[inline]
92    pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
93        self.set.contains(class.as_ref())
94    }
95
96    /// Check the set is empty.
97    #[inline]
98    pub fn is_empty(&self) -> bool {
99        self.set.is_empty()
100    }
101}
102
103impl IntoPropValue<AttrValue> for Classes {
104    #[inline]
105    fn into_prop_value(self) -> AttrValue {
106        let mut classes = self.set.iter().cloned();
107
108        match classes.next() {
109            None => AttrValue::Static(""),
110            Some(class) if classes.len() == 0 => class,
111            Some(first) => build_attr_value(first, classes),
112        }
113    }
114}
115
116impl IntoPropValue<Option<AttrValue>> for Classes {
117    #[inline]
118    fn into_prop_value(self) -> Option<AttrValue> {
119        if self.is_empty() {
120            None
121        } else {
122            Some(self.into_prop_value())
123        }
124    }
125}
126
127impl IntoPropValue<Classes> for &'static str {
128    #[inline]
129    fn into_prop_value(self) -> Classes {
130        self.into()
131    }
132}
133
134impl<T: Into<Classes>> Extend<T> for Classes {
135    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
136        iter.into_iter().for_each(|classes| self.push(classes))
137    }
138}
139
140impl<T: Into<Classes>> FromIterator<T> for Classes {
141    fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
142        let mut classes = Self::new();
143        classes.extend(iter);
144        classes
145    }
146}
147
148impl IntoIterator for Classes {
149    type IntoIter = indexmap::set::IntoIter<AttrValue>;
150    type Item = AttrValue;
151
152    #[inline]
153    fn into_iter(self) -> Self::IntoIter {
154        RcExt::unwrap_or_clone(self.set).into_iter()
155    }
156}
157
158impl IntoIterator for &Classes {
159    type IntoIter = indexmap::set::IntoIter<AttrValue>;
160    type Item = AttrValue;
161
162    #[inline]
163    fn into_iter(self) -> Self::IntoIter {
164        (*self.set).clone().into_iter()
165    }
166}
167
168#[allow(clippy::to_string_trait_impl)]
169impl ToString for Classes {
170    fn to_string(&self) -> String {
171        let mut iter = self.set.iter().cloned();
172
173        iter.next()
174            .map(|first| build_attr_value(first, iter))
175            .unwrap_or_default()
176            .to_string()
177    }
178}
179
180impl From<Cow<'static, str>> for Classes {
181    fn from(t: Cow<'static, str>) -> Self {
182        match t {
183            Cow::Borrowed(x) => Self::from(x),
184            Cow::Owned(x) => Self::from(x),
185        }
186    }
187}
188
189impl From<&'static str> for Classes {
190    fn from(t: &'static str) -> Self {
191        let set = t.split_whitespace().map(AttrValue::Static).collect();
192        Self { set: Rc::new(set) }
193    }
194}
195
196impl From<String> for Classes {
197    fn from(t: String) -> Self {
198        match t.contains(|c: char| c.is_whitespace()) {
199            // If the string only contains a single class, we can just use it
200            // directly (rather than cloning it into a new string). Need to make
201            // sure it's not empty, though.
202            false => match t.is_empty() {
203                true => Self::new(),
204                false => Self {
205                    set: Rc::new(IndexSet::from_iter([AttrValue::from(t)])),
206                },
207            },
208            true => Self::from(&t),
209        }
210    }
211}
212
213impl From<&String> for Classes {
214    fn from(t: &String) -> Self {
215        let set = t
216            .split_whitespace()
217            .map(ToOwned::to_owned)
218            .map(AttrValue::from)
219            .collect();
220        Self { set: Rc::new(set) }
221    }
222}
223
224impl From<&AttrValue> for Classes {
225    fn from(t: &AttrValue) -> Self {
226        let set = t
227            .split_whitespace()
228            .map(ToOwned::to_owned)
229            .map(AttrValue::from)
230            .collect();
231        Self { set: Rc::new(set) }
232    }
233}
234
235impl From<AttrValue> for Classes {
236    fn from(t: AttrValue) -> Self {
237        match t.contains(|c: char| c.is_whitespace()) {
238            // If the string only contains a single class, we can just use it
239            // directly (rather than cloning it into a new string). Need to make
240            // sure it's not empty, though.
241            false => match t.is_empty() {
242                true => Self::new(),
243                false => Self {
244                    set: Rc::new(IndexSet::from_iter([t])),
245                },
246            },
247            true => Self::from(&t),
248        }
249    }
250}
251
252impl<T: Into<Classes>> From<Option<T>> for Classes {
253    fn from(t: Option<T>) -> Self {
254        t.map(|x| x.into()).unwrap_or_default()
255    }
256}
257
258impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
259    fn from(t: &Option<T>) -> Self {
260        Self::from(t.clone())
261    }
262}
263
264impl<T: Into<Classes>> From<Vec<T>> for Classes {
265    fn from(t: Vec<T>) -> Self {
266        Self::from_iter(t)
267    }
268}
269
270impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
271    fn from(t: &[T]) -> Self {
272        t.iter().cloned().collect()
273    }
274}
275
276impl<T: Into<Classes>, const SIZE: usize> From<[T; SIZE]> for Classes {
277    fn from(t: [T; SIZE]) -> Self {
278        t.into_iter().collect()
279    }
280}
281
282impl PartialEq for Classes {
283    fn eq(&self, other: &Self) -> bool {
284        self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
285    }
286}
287
288impl Eq for Classes {}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    struct TestClass;
295
296    impl TestClass {
297        fn as_class(&self) -> &'static str {
298            "test-class"
299        }
300    }
301
302    impl From<TestClass> for Classes {
303        fn from(x: TestClass) -> Self {
304            Classes::from(x.as_class())
305        }
306    }
307
308    #[test]
309    fn it_is_initially_empty() {
310        let subject = Classes::new();
311        assert!(subject.is_empty());
312    }
313
314    #[test]
315    fn it_pushes_value() {
316        let mut subject = Classes::new();
317        subject.push("foo");
318        assert!(!subject.is_empty());
319        assert!(subject.contains("foo"));
320    }
321
322    #[test]
323    fn it_adds_values_via_extend() {
324        let mut other = Classes::new();
325        other.push("bar");
326        let mut subject = Classes::new();
327        subject.extend(other);
328        assert!(subject.contains("bar"));
329    }
330
331    #[test]
332    fn it_contains_both_values() {
333        let mut other = Classes::new();
334        other.push("bar");
335        let mut subject = Classes::new();
336        subject.extend(other);
337        subject.push("foo");
338        assert!(subject.contains("foo"));
339        assert!(subject.contains("bar"));
340    }
341
342    #[test]
343    fn it_splits_class_with_spaces() {
344        let mut subject = Classes::new();
345        subject.push("foo bar");
346        assert!(subject.contains("foo"));
347        assert!(subject.contains("bar"));
348    }
349
350    #[test]
351    fn push_and_contains_can_be_used_with_other_objects() {
352        let mut subject = Classes::new();
353        subject.push(TestClass);
354        let other_class: Option<TestClass> = None;
355        subject.push(other_class);
356        assert!(subject.contains(TestClass.as_class()));
357    }
358
359    #[test]
360    fn can_be_extended_with_another_class() {
361        let mut other = Classes::new();
362        other.push("foo");
363        other.push("bar");
364        let mut subject = Classes::new();
365        subject.extend(&other);
366        subject.extend(other);
367        assert!(subject.contains("foo"));
368        assert!(subject.contains("bar"));
369    }
370
371    #[test]
372    fn can_be_collected() {
373        let classes = vec!["foo", "bar"];
374        let subject = classes.into_iter().collect::<Classes>();
375        assert!(subject.contains("foo"));
376        assert!(subject.contains("bar"));
377    }
378
379    #[test]
380    fn ignores_empty_string() {
381        let classes = String::from("");
382        let subject = Classes::from(classes);
383        assert!(subject.is_empty())
384    }
385}