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