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