yew_stdweb/html/
classes.rs

1use super::{IntoOptPropValue, IntoPropValue};
2use crate::virtual_dom::AttrValue;
3use indexmap::IndexSet;
4use std::{
5    borrow::{Borrow, Cow},
6    iter::FromIterator,
7};
8
9/// A set of classes.
10///
11/// The preferred way of creating this is using the [`classes!`][yew::classes!] macro.
12#[derive(Debug, Clone, Default)]
13pub struct Classes {
14    set: IndexSet<Cow<'static, str>>,
15}
16
17impl Classes {
18    /// Creates an empty set of classes. (Does not allocate.)
19    pub fn new() -> Self {
20        Self {
21            set: IndexSet::new(),
22        }
23    }
24
25    /// Creates an empty set of classes with capacity for n elements. (Does not allocate if n is
26    /// zero.)
27    pub fn with_capacity(n: usize) -> Self {
28        Self {
29            set: IndexSet::with_capacity(n),
30        }
31    }
32
33    /// Adds a class to a set.
34    ///
35    /// If the provided class has already been added, this method will ignore it.
36    pub fn push<T: Into<Self>>(&mut self, class: T) {
37        let classes_to_add: Self = class.into();
38        self.set.extend(classes_to_add.set);
39    }
40
41    /// Adds a class to a set.
42    ///
43    /// If the provided class has already been added, this method will ignore it.
44    ///
45    /// This method won't check if there are multiple classes in the input string.
46    ///
47    /// # Safety
48    ///
49    /// This function will not split the string into multiple classes. Please do not use it unless
50    /// you are absolutely certain that the string does not contain any whitespace. Using `push()`
51    /// is preferred.
52    pub unsafe fn unchecked_push<T: Into<Cow<'static, str>>>(&mut self, class: T) {
53        self.set.insert(class.into());
54    }
55
56    /// Check the set contains a class.
57    pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
58        self.set.contains(class.as_ref())
59    }
60
61    /// Check the set is empty.
62    pub fn is_empty(&self) -> bool {
63        self.set.is_empty()
64    }
65}
66
67impl IntoPropValue<AttrValue> for Classes {
68    fn into_prop_value(mut self) -> AttrValue {
69        if self.set.len() == 1 {
70            self.set.pop().unwrap()
71        } else {
72            Cow::Owned(self.to_string())
73        }
74    }
75}
76
77impl IntoOptPropValue<AttrValue> for Classes {
78    fn into_opt_prop_value(self) -> Option<AttrValue> {
79        if self.is_empty() {
80            None
81        } else {
82            Some(self.into_prop_value())
83        }
84    }
85}
86
87impl<T: Into<Classes>> Extend<T> for Classes {
88    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
89        let classes = iter
90            .into_iter()
91            .map(Into::into)
92            .flat_map(|classes| classes.set);
93        self.set.extend(classes);
94    }
95}
96
97impl<T: Into<Classes>> FromIterator<T> for Classes {
98    fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
99        let mut classes = Self::new();
100        classes.extend(iter);
101        classes
102    }
103}
104
105impl IntoIterator for Classes {
106    type Item = Cow<'static, str>;
107    type IntoIter = indexmap::set::IntoIter<Cow<'static, str>>;
108
109    fn into_iter(self) -> Self::IntoIter {
110        self.set.into_iter()
111    }
112}
113
114impl ToString for Classes {
115    fn to_string(&self) -> String {
116        self.set
117            .iter()
118            .map(Borrow::borrow)
119            .collect::<Vec<_>>()
120            .join(" ")
121    }
122}
123
124impl From<Cow<'static, str>> for Classes {
125    fn from(t: Cow<'static, str>) -> Self {
126        match t {
127            Cow::Borrowed(x) => Self::from(x),
128            Cow::Owned(x) => Self::from(x),
129        }
130    }
131}
132
133impl From<&'static str> for Classes {
134    fn from(t: &'static str) -> Self {
135        let set = t.split_whitespace().map(Cow::Borrowed).collect();
136        Self { set }
137    }
138}
139
140impl From<String> for Classes {
141    fn from(t: String) -> Self {
142        Self::from(&t)
143    }
144}
145
146impl From<&String> for Classes {
147    fn from(t: &String) -> Self {
148        let set = t
149            .split_whitespace()
150            .map(ToOwned::to_owned)
151            .map(Cow::Owned)
152            .collect();
153        Self { set }
154    }
155}
156
157impl<T: Into<Classes>> From<Option<T>> for Classes {
158    fn from(t: Option<T>) -> Self {
159        t.map(|x| x.into()).unwrap_or_default()
160    }
161}
162
163impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
164    fn from(t: &Option<T>) -> Self {
165        Self::from(t.clone())
166    }
167}
168
169impl<T: Into<Classes>> From<Vec<T>> for Classes {
170    fn from(t: Vec<T>) -> Self {
171        Self::from_iter(t)
172    }
173}
174
175impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
176    fn from(t: &[T]) -> Self {
177        Self::from_iter(t.iter().cloned())
178    }
179}
180
181impl PartialEq for Classes {
182    fn eq(&self, other: &Self) -> bool {
183        self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    struct TestClass;
192
193    impl TestClass {
194        fn as_class(&self) -> &'static str {
195            "test-class"
196        }
197    }
198
199    impl From<TestClass> for Classes {
200        fn from(x: TestClass) -> Self {
201            Classes::from(x.as_class())
202        }
203    }
204
205    #[test]
206    fn it_is_initially_empty() {
207        let subject = Classes::new();
208        assert!(subject.is_empty());
209    }
210
211    #[test]
212    fn it_pushes_value() {
213        let mut subject = Classes::new();
214        subject.push("foo");
215        assert!(!subject.is_empty());
216        assert!(subject.contains("foo"));
217    }
218
219    #[test]
220    fn it_adds_values_via_extend() {
221        let mut other = Classes::new();
222        other.push("bar");
223        let mut subject = Classes::new();
224        subject.extend(other);
225        assert!(subject.contains("bar"));
226    }
227
228    #[test]
229    fn it_contains_both_values() {
230        let mut other = Classes::new();
231        other.push("bar");
232        let mut subject = Classes::new();
233        subject.extend(other);
234        subject.push("foo");
235        assert!(subject.contains("foo"));
236        assert!(subject.contains("bar"));
237    }
238
239    #[test]
240    fn it_splits_class_with_spaces() {
241        let mut subject = Classes::new();
242        subject.push("foo bar");
243        assert!(subject.contains("foo"));
244        assert!(subject.contains("bar"));
245    }
246
247    #[test]
248    fn push_and_contains_can_be_used_with_other_objects() {
249        let mut subject = Classes::new();
250        subject.push(TestClass);
251        let other_class: Option<TestClass> = None;
252        subject.push(other_class);
253        assert!(subject.contains(TestClass.as_class()));
254    }
255
256    #[test]
257    fn can_be_extended_with_another_class() {
258        let mut other = Classes::new();
259        other.push("foo");
260        other.push("bar");
261        let mut subject = Classes::new();
262        subject.extend(other);
263        assert!(subject.contains("foo"));
264        assert!(subject.contains("bar"));
265    }
266
267    #[test]
268    fn can_be_collected() {
269        let classes = vec!["foo", "bar"];
270        let subject = classes.into_iter().collect::<Classes>();
271        assert!(subject.contains("foo"));
272        assert!(subject.contains("bar"));
273    }
274}