Skip to main content

tachys/html/
class.rs

1use super::attribute::{
2    maybe_next_attr_erasure_macros::next_attr_output_type, Attribute,
3    NamedAttributeKey, NextAttribute,
4};
5use crate::{
6    html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
7    renderer::Rndr,
8    view::{Position, ToTemplate},
9};
10use std::{borrow::Cow, future::Future, sync::Arc};
11
12/// Adds a CSS class.
13#[inline(always)]
14pub fn class<C>(class: C) -> Class<C>
15where
16    C: IntoClass,
17{
18    Class { class }
19}
20
21/// A CSS class.
22#[derive(Debug)]
23pub struct Class<C> {
24    class: C,
25}
26
27impl<C> Clone for Class<C>
28where
29    C: Clone,
30{
31    fn clone(&self) -> Self {
32        Self {
33            class: self.class.clone(),
34        }
35    }
36}
37
38impl<C> Attribute for Class<C>
39where
40    C: IntoClass,
41{
42    const MIN_LENGTH: usize = C::MIN_LENGTH;
43
44    type AsyncOutput = Class<C::AsyncOutput>;
45    type State = C::State;
46    type Cloneable = Class<C::Cloneable>;
47    type CloneableOwned = Class<C::CloneableOwned>;
48
49    fn html_len(&self) -> usize {
50        self.class.html_len() + 1
51    }
52
53    fn to_html(
54        self,
55        _buf: &mut String,
56        class: &mut String,
57        _style: &mut String,
58        _inner_html: &mut String,
59    ) {
60        // If this is a class="..." attribute (not class:name=value), clear previous value
61        if self.class.should_overwrite() {
62            class.clear();
63        }
64        class.push(' ');
65        self.class.to_html(class);
66    }
67
68    fn hydrate<const FROM_SERVER: bool>(
69        self,
70        el: &crate::renderer::types::Element,
71    ) -> Self::State {
72        self.class.hydrate::<FROM_SERVER>(el)
73    }
74
75    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
76        self.class.build(el)
77    }
78
79    fn rebuild(self, state: &mut Self::State) {
80        self.class.rebuild(state)
81    }
82
83    fn into_cloneable(self) -> Self::Cloneable {
84        Class {
85            class: self.class.into_cloneable(),
86        }
87    }
88
89    fn into_cloneable_owned(self) -> Self::CloneableOwned {
90        Class {
91            class: self.class.into_cloneable_owned(),
92        }
93    }
94
95    fn dry_resolve(&mut self) {
96        self.class.dry_resolve();
97    }
98
99    async fn resolve(self) -> Self::AsyncOutput {
100        Class {
101            class: self.class.resolve().await,
102        }
103    }
104
105    fn keys(&self) -> Vec<NamedAttributeKey> {
106        vec![NamedAttributeKey::Attribute("class".into())]
107    }
108}
109
110impl<C> NextAttribute for Class<C>
111where
112    C: IntoClass,
113{
114    next_attr_output_type!(Self, NewAttr);
115
116    fn add_any_attr<NewAttr: Attribute>(
117        self,
118        new_attr: NewAttr,
119    ) -> Self::Output<NewAttr> {
120        next_attr_combine!(self, new_attr)
121    }
122}
123
124impl<C> ToTemplate for Class<C>
125where
126    C: IntoClass,
127{
128    const CLASS: &'static str = C::TEMPLATE;
129
130    fn to_template(
131        _buf: &mut String,
132        class: &mut String,
133        _style: &mut String,
134        _inner_html: &mut String,
135        _position: &mut Position,
136    ) {
137        C::to_template(class);
138    }
139}
140
141/// A possible value for a CSS class.
142pub trait IntoClass: Send {
143    /// The HTML that should be included in a `<template>`.
144    const TEMPLATE: &'static str = "";
145    /// The minimum length of the HTML.
146    const MIN_LENGTH: usize = Self::TEMPLATE.len();
147
148    /// The type after all async data have resolved.
149    type AsyncOutput: IntoClass;
150    /// The view state retained between building and rebuilding.
151    type State;
152    /// An equivalent value that can be cloned.
153    type Cloneable: IntoClass + Clone;
154    /// An equivalent value that can be cloned and is `'static`.
155    type CloneableOwned: IntoClass + Clone + 'static;
156
157    /// The estimated length of the HTML.
158    fn html_len(&self) -> usize;
159
160    /// Renders the class to HTML.
161    fn to_html(self, class: &mut String);
162
163    /// Whether this class attribute should overwrite previous class values.
164    /// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
165    fn should_overwrite(&self) -> bool {
166        false
167    }
168
169    /// Renders the class to HTML for a `<template>`.
170    #[allow(unused)] // it's used with `nightly` feature
171    fn to_template(class: &mut String) {}
172
173    /// Adds interactivity as necessary, given DOM nodes that were created from HTML that has
174    /// either been rendered on the server, or cloned for a `<template>`.
175    fn hydrate<const FROM_SERVER: bool>(
176        self,
177        el: &crate::renderer::types::Element,
178    ) -> Self::State;
179
180    /// Adds this class to the element during client-side rendering.
181    fn build(self, el: &crate::renderer::types::Element) -> Self::State;
182
183    /// Updates the value.
184    fn rebuild(self, state: &mut Self::State);
185
186    /// Converts this to a cloneable type.
187    fn into_cloneable(self) -> Self::Cloneable;
188
189    /// Converts this to a cloneable, owned type.
190    fn into_cloneable_owned(self) -> Self::CloneableOwned;
191
192    /// “Runs” the attribute without other side effects. For primitive types, this is a no-op. For
193    /// reactive types, this can be used to gather data about reactivity or about asynchronous data
194    /// that needs to be loaded.
195    fn dry_resolve(&mut self);
196
197    /// “Resolves” this into a type that is not waiting for any asynchronous data.
198    fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
199
200    /// Reset the class list to the state before this class was added.
201    fn reset(state: &mut Self::State);
202}
203
204impl<T: IntoClass> IntoClass for Option<T> {
205    type AsyncOutput = Option<T::AsyncOutput>;
206    type State = (crate::renderer::types::Element, Option<T::State>);
207    type Cloneable = Option<T::Cloneable>;
208    type CloneableOwned = Option<T::CloneableOwned>;
209
210    fn html_len(&self) -> usize {
211        self.as_ref().map_or(0, IntoClass::html_len)
212    }
213
214    fn to_html(self, class: &mut String) {
215        if let Some(t) = self {
216            t.to_html(class);
217        }
218    }
219
220    fn hydrate<const FROM_SERVER: bool>(
221        self,
222        el: &crate::renderer::types::Element,
223    ) -> Self::State {
224        if let Some(t) = self {
225            (el.clone(), Some(t.hydrate::<FROM_SERVER>(el)))
226        } else {
227            (el.clone(), None)
228        }
229    }
230
231    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
232        if let Some(t) = self {
233            (el.clone(), Some(t.build(el)))
234        } else {
235            (el.clone(), None)
236        }
237    }
238
239    fn rebuild(self, state: &mut Self::State) {
240        let el = &state.0;
241        let prev_state = &mut state.1;
242        let maybe_next_t_state = match (prev_state.take(), self) {
243            (Some(mut prev_t_state), None) => {
244                T::reset(&mut prev_t_state);
245                Some(None)
246            }
247            (None, Some(t)) => Some(Some(t.build(el))),
248            (Some(mut prev_t_state), Some(t)) => {
249                t.rebuild(&mut prev_t_state);
250                Some(Some(prev_t_state))
251            }
252            (None, None) => Some(None),
253        };
254        if let Some(next_t_state) = maybe_next_t_state {
255            state.1 = next_t_state;
256        }
257    }
258
259    fn into_cloneable(self) -> Self::Cloneable {
260        self.map(|t| t.into_cloneable())
261    }
262
263    fn into_cloneable_owned(self) -> Self::CloneableOwned {
264        self.map(|t| t.into_cloneable_owned())
265    }
266
267    fn dry_resolve(&mut self) {
268        if let Some(t) = self {
269            t.dry_resolve();
270        }
271    }
272
273    async fn resolve(self) -> Self::AsyncOutput {
274        if let Some(t) = self {
275            Some(t.resolve().await)
276        } else {
277            None
278        }
279    }
280
281    fn reset(state: &mut Self::State) {
282        if let Some(prev_t_state) = &mut state.1 {
283            T::reset(prev_t_state);
284        }
285    }
286}
287
288impl IntoClass for &str {
289    type AsyncOutput = Self;
290    type State = (crate::renderer::types::Element, Self);
291    type Cloneable = Self;
292    type CloneableOwned = Arc<str>;
293
294    fn html_len(&self) -> usize {
295        self.len()
296    }
297
298    fn to_html(self, class: &mut String) {
299        class.push_str(self);
300    }
301
302    fn should_overwrite(&self) -> bool {
303        true
304    }
305
306    fn hydrate<const FROM_SERVER: bool>(
307        self,
308        el: &crate::renderer::types::Element,
309    ) -> Self::State {
310        if !FROM_SERVER {
311            Rndr::set_attribute(el, "class", self);
312        }
313        (el.clone(), self)
314    }
315
316    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
317        Rndr::set_attribute(el, "class", self);
318        (el.clone(), self)
319    }
320
321    fn rebuild(self, state: &mut Self::State) {
322        let (el, prev) = state;
323        if self != *prev {
324            Rndr::set_attribute(el, "class", self);
325        }
326        *prev = self;
327    }
328
329    fn into_cloneable(self) -> Self::Cloneable {
330        self
331    }
332
333    fn into_cloneable_owned(self) -> Self::CloneableOwned {
334        self.into()
335    }
336
337    fn dry_resolve(&mut self) {}
338
339    async fn resolve(self) -> Self::AsyncOutput {
340        self
341    }
342
343    fn reset(state: &mut Self::State) {
344        let (el, _prev) = state;
345        Rndr::remove_attribute(el, "class");
346    }
347}
348
349impl IntoClass for Cow<'_, str> {
350    type AsyncOutput = Self;
351    type State = (crate::renderer::types::Element, Self);
352    type Cloneable = Arc<str>;
353    type CloneableOwned = Arc<str>;
354
355    fn html_len(&self) -> usize {
356        self.len()
357    }
358
359    fn to_html(self, class: &mut String) {
360        IntoClass::to_html(&*self, class);
361    }
362
363    fn should_overwrite(&self) -> bool {
364        true
365    }
366
367    fn hydrate<const FROM_SERVER: bool>(
368        self,
369        el: &crate::renderer::types::Element,
370    ) -> Self::State {
371        if !FROM_SERVER {
372            Rndr::set_attribute(el, "class", &self);
373        }
374        (el.clone(), self)
375    }
376
377    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
378        Rndr::set_attribute(el, "class", &self);
379        (el.clone(), self)
380    }
381
382    fn rebuild(self, state: &mut Self::State) {
383        let (el, prev) = state;
384        if self != *prev {
385            Rndr::set_attribute(el, "class", &self);
386        }
387        *prev = self;
388    }
389
390    fn into_cloneable(self) -> Self::Cloneable {
391        self.into()
392    }
393
394    fn into_cloneable_owned(self) -> Self::CloneableOwned {
395        self.into()
396    }
397
398    fn dry_resolve(&mut self) {}
399
400    async fn resolve(self) -> Self::AsyncOutput {
401        self
402    }
403
404    fn reset(state: &mut Self::State) {
405        let (el, _prev) = state;
406        Rndr::remove_attribute(el, "class");
407    }
408}
409
410impl IntoClass for String {
411    type AsyncOutput = Self;
412    type State = (crate::renderer::types::Element, Self);
413    type Cloneable = Arc<str>;
414    type CloneableOwned = Arc<str>;
415
416    fn html_len(&self) -> usize {
417        self.len()
418    }
419
420    fn to_html(self, class: &mut String) {
421        IntoClass::to_html(self.as_str(), class);
422    }
423
424    fn should_overwrite(&self) -> bool {
425        true
426    }
427
428    fn hydrate<const FROM_SERVER: bool>(
429        self,
430        el: &crate::renderer::types::Element,
431    ) -> Self::State {
432        if !FROM_SERVER {
433            Rndr::set_attribute(el, "class", &self);
434        }
435        (el.clone(), self)
436    }
437
438    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
439        Rndr::set_attribute(el, "class", &self);
440        (el.clone(), self)
441    }
442
443    fn rebuild(self, state: &mut Self::State) {
444        let (el, prev) = state;
445        if self != *prev {
446            Rndr::set_attribute(el, "class", &self);
447        }
448        *prev = self;
449    }
450
451    fn into_cloneable(self) -> Self::Cloneable {
452        self.into()
453    }
454
455    fn into_cloneable_owned(self) -> Self::CloneableOwned {
456        self.into()
457    }
458
459    fn dry_resolve(&mut self) {}
460
461    async fn resolve(self) -> Self::AsyncOutput {
462        self
463    }
464
465    fn reset(state: &mut Self::State) {
466        let (el, _prev) = state;
467        Rndr::remove_attribute(el, "class");
468    }
469}
470
471impl IntoClass for Arc<str> {
472    type AsyncOutput = Self;
473    type State = (crate::renderer::types::Element, Self);
474    type Cloneable = Self;
475    type CloneableOwned = Self;
476
477    fn html_len(&self) -> usize {
478        self.len()
479    }
480
481    fn to_html(self, class: &mut String) {
482        IntoClass::to_html(self.as_ref(), class);
483    }
484
485    fn should_overwrite(&self) -> bool {
486        true
487    }
488
489    fn hydrate<const FROM_SERVER: bool>(
490        self,
491        el: &crate::renderer::types::Element,
492    ) -> Self::State {
493        if !FROM_SERVER {
494            Rndr::set_attribute(el, "class", &self);
495        }
496        (el.clone(), self)
497    }
498
499    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
500        Rndr::set_attribute(el, "class", &self);
501        (el.clone(), self)
502    }
503
504    fn rebuild(self, state: &mut Self::State) {
505        let (el, prev) = state;
506        if self != *prev {
507            Rndr::set_attribute(el, "class", &self);
508        }
509        *prev = self;
510    }
511
512    fn into_cloneable(self) -> Self::Cloneable {
513        self
514    }
515
516    fn into_cloneable_owned(self) -> Self::CloneableOwned {
517        self
518    }
519
520    fn dry_resolve(&mut self) {}
521
522    async fn resolve(self) -> Self::AsyncOutput {
523        self
524    }
525
526    fn reset(state: &mut Self::State) {
527        let (el, _prev) = state;
528        Rndr::remove_attribute(el, "class");
529    }
530}
531
532impl IntoClass for (&'static str, bool) {
533    type AsyncOutput = Self;
534    type State = (crate::renderer::types::ClassList, bool, &'static str);
535    type Cloneable = Self;
536    type CloneableOwned = Self;
537
538    fn html_len(&self) -> usize {
539        self.0.len()
540    }
541
542    fn to_html(self, class: &mut String) {
543        let (name, include) = self;
544        if include {
545            class.push_str(name);
546        }
547    }
548
549    fn hydrate<const FROM_SERVER: bool>(
550        self,
551        el: &crate::renderer::types::Element,
552    ) -> Self::State {
553        let (name, include) = self;
554        let class_list = Rndr::class_list(el);
555        if !FROM_SERVER && include {
556            Rndr::add_class(&class_list, name);
557        }
558        (class_list, self.1, name)
559    }
560
561    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
562        let (name, include) = self;
563        let class_list = Rndr::class_list(el);
564        if include {
565            Rndr::add_class(&class_list, name);
566        }
567        (class_list, self.1, name)
568    }
569
570    fn rebuild(self, state: &mut Self::State) {
571        let (name, include) = self;
572        let (class_list, prev_include, prev_name) = state;
573        if name == *prev_name {
574            if include != *prev_include {
575                if include {
576                    Rndr::add_class(class_list, name);
577                } else {
578                    Rndr::remove_class(class_list, name);
579                }
580            }
581        } else {
582            if *prev_include {
583                Rndr::remove_class(class_list, prev_name);
584            }
585            if include {
586                Rndr::add_class(class_list, name);
587            }
588        }
589        *prev_include = include;
590        *prev_name = name;
591    }
592
593    fn into_cloneable(self) -> Self::Cloneable {
594        self
595    }
596
597    fn into_cloneable_owned(self) -> Self::Cloneable {
598        self
599    }
600
601    fn dry_resolve(&mut self) {}
602
603    async fn resolve(self) -> Self::AsyncOutput {
604        self
605    }
606
607    fn reset(state: &mut Self::State) {
608        let (class_list, _, name) = state;
609        Rndr::remove_class(class_list, name);
610    }
611}
612
613#[cfg(all(feature = "nightly", rustc_nightly))]
614impl<const V: &'static str> IntoClass for crate::view::static_types::Static<V> {
615    const TEMPLATE: &'static str = V;
616
617    type AsyncOutput = Self;
618    type State = ();
619    type Cloneable = Self;
620    type CloneableOwned = Self;
621
622    fn html_len(&self) -> usize {
623        V.len()
624    }
625
626    fn to_html(self, class: &mut String) {
627        class.push_str(V);
628    }
629
630    fn to_template(class: &mut String) {
631        class.push_str(V);
632    }
633
634    fn hydrate<const FROM_SERVER: bool>(
635        self,
636        _el: &crate::renderer::types::Element,
637    ) -> Self::State {
638    }
639
640    fn build(self, el: &crate::renderer::types::Element) -> Self::State {
641        Rndr::set_attribute(el, "class", V);
642    }
643
644    fn rebuild(self, _state: &mut Self::State) {}
645
646    fn into_cloneable(self) -> Self::Cloneable {
647        self
648    }
649
650    fn into_cloneable_owned(self) -> Self::CloneableOwned {
651        self
652    }
653
654    fn dry_resolve(&mut self) {}
655
656    async fn resolve(self) -> Self::AsyncOutput {
657        self
658    }
659
660    fn reset(_state: &mut Self::State) {}
661}
662
663/* #[cfg(test)]
664mod tests {
665    use crate::{
666        html::{
667            class::class,
668            element::{p, HtmlElement},
669        },
670        renderer::dom::Dom,
671        view::{Position, PositionState, RenderHtml},
672    };
673
674    #[test]
675    fn adds_simple_class() {
676        let mut html = String::new();
677        let el: HtmlElement<_, _, _, Dom> = p(class("foo bar"), ());
678        el.to_html(&mut html, &PositionState::new(Position::FirstChild));
679
680        assert_eq!(html, r#"<p class="foo bar"></p>"#);
681    }
682
683    #[test]
684    fn adds_class_with_dynamic() {
685        let mut html = String::new();
686        let el: HtmlElement<_, _, _, Dom> =
687            p((class("foo bar"), class(("baz", true))), ());
688        el.to_html(&mut html, &PositionState::new(Position::FirstChild));
689
690        assert_eq!(html, r#"<p class="foo bar baz"></p>"#);
691    }
692
693    #[test]
694    fn adds_class_with_dynamic_and_function() {
695        let mut html = String::new();
696        let el: HtmlElement<_, _, _, Dom> = p(
697            (
698                class("foo bar"),
699                class(("baz", || true)),
700                class(("boo", false)),
701            ),
702            (),
703        );
704        el.to_html(&mut html, &PositionState::new(Position::FirstChild));
705
706        assert_eq!(html, r#"<p class="foo bar baz"></p>"#);
707    }
708} */