maud-extensions 0.6.7

Component, inline CSS/JS, and font helper macros for Maud views.
Documentation
#![cfg(feature = "components")]

use maud::{Markup, Render, html};
use maud_extensions::{Component, Slot};

#[derive(Component, Debug)]
struct Badge {
    label: String,
    tone: Option<String>,
    count: usize,
}

#[derive(Component, Debug)]
struct Card {
    title: String,
    header: Slot<Markup>,
    #[mx(default)]
    body: Slot<Markup>,
    footer: Slot<Markup>,
    #[mx(each = action)]
    actions: Slot<Vec<Markup>>,
}

impl Badge {
    fn css() -> Markup {
        maud::PreEscaped(String::new())
    }

    fn js() -> Markup {
        maud::PreEscaped(String::new())
    }
}

impl Render for Badge {
    fn render(&self) -> Markup {
        html! {
            span class="badge" {
                (Self::css())
                (Self::js())
                (self.label)
                @if let Some(tone) = &self.tone {
                    " "
                    (tone)
                }
            }
        }
    }
}

impl Card {
    fn css() -> Markup {
        maud_extensions::css! {
            me .actions {
                display: flex;
                gap: 0.5rem;
            }
        }
    }

    fn js() -> Markup {
        maud_extensions::js!(once, {
            me().class_add("ready");
        })
    }
}

impl Render for Card {
    fn render(&self) -> Markup {
        html! {
            article class="card" {
                (Self::css())
                (Self::js())
                header class="header" { (self.header) }
                h2 { (self.title) }
                div class="body" { (self.body) }
                footer class="footer" { (self.footer) }
                div class="actions" { (self.actions) }
            }
        }
    }
}

#[test]
fn component_v1_uses_bon_backed_new_and_build() {
    let badge = Badge::new().label("New").tone("warm").count(0).build();

    assert_eq!(badge.label, "New");
    assert_eq!(badge.tone.as_deref(), Some("warm"));
    assert_eq!(badge.count, 0);
}



#[test]
fn component_v1_supports_optional_props_by_absence() {
    let badge = Badge::new().label("Stable").count(0).build();

    assert_eq!(badge.label, "Stable");
    assert_eq!(badge.tone, None);
    assert_eq!(badge.count, 0);
}

#[test]
fn component_v1_builder_can_render_when_component_implements_render() {
    let markup = Badge::new()
        .label("Live")
        .tone("warm")
        .count(0)
        .render()
        .into_string();

    assert!(markup.contains("<span class=\"badge\">Live warm</span>"));
}

#[test]
fn component_v1_default_slot_supports_child_alias() {
    let markup = Card::new()
        .title("Settings")
        .child(html! { p { "Profile details" } })
        .render()
        .into_string();

    assert!(markup.contains("<h2>Settings</h2>"));
    assert!(markup.contains("<div class=\"body\"><p>Profile details</p></div>"));
}

#[test]
fn component_v1_default_slot_supports_named_slot_setter_accepting_render() {
    let card = Card::new()
        .title("Account")
        .body(html! { strong { "Details" } })
        .build();

    let markup = card.render().into_string();
    assert!(markup.contains("<div class=\"body\"><strong>Details</strong></div>"));
}

#[test]
fn component_v1_named_optional_slots_accept_renderables() {
    let markup = Card::new()
        .title("Profile")
        .header(html! { span { "Welcome" } })
        .child(html! { p { "Body" } })
        .footer(html! { button { "Save" } })
        .render()
        .into_string();

    assert!(markup.contains("<header class=\"header\"><span>Welcome</span></header>"));
    assert!(markup.contains("<footer class=\"footer\"><button>Save</button></footer>"));
}

#[test]
fn component_v1_repeated_slots_support_each_style_renderable_setters() {
    let markup = Card::new()
        .title("Profile")
        .header(html! { em { "Heads up" } })
        .child(html! { p { "Body" } })
        .action(html! { button { "Save" } })
        .action(html! { button { "Cancel" } })
        .render()
        .into_string();

    assert!(markup.contains("<header class=\"header\"><em>Heads up</em></header>"));
    assert!(
        markup
            .contains("<div class=\"actions\"><button>Save</button><button>Cancel</button></div>")
    );
}

#[test]
fn component_v1_repeated_slots_keep_bulk_vec_setter() {
    let card = Card::new()
        .title("Profile")
        .body(html! { p { "Body" } })
        .header(html! { span { "Welcome" } })
        .footer(html! { button { "Save" } })
        .actions(vec![html! { button { "One" } }, html! { button { "Two" } }])
        .build();

    let markup = card.render().into_string();
    assert!(
        markup.contains("<div class=\"actions\"><button>One</button><button>Two</button></div>")
    );
}