Views

Plain text won't sell many tickets. Time for real pages. Views render HTML with maud, a template language that is just Rust. Add maud to your dependencies.

Writing a layout

A view is a singleton that implements RendersView. Its props are a struct you define. Every page of the box office shares this layout:

use margaret::framework::macros::renders_view;
use margaret::framework::macros::singleton;
use margaret::framework::views::maud::DOCTYPE;
use margaret::framework::views::maud::Markup;
use margaret::framework::views::maud::html;
use margaret::framework::views::renders_view::RendersView;

pub struct LayoutProps {
    pub title: String,
    pub content: Markup,
}

#[singleton]
#[renders_view(name = "layout")]
pub struct Layout;

impl RendersView for Layout {
    type Props<'props> = LayoutProps;

    fn render(&self, LayoutProps { title, content }: Self::Props<'_>) -> anyhow::Result<Markup> {
        Ok(html! {
            (DOCTYPE)
            html lang="en" {
                head {
                    meta charset="utf-8";
                    title { (title) " · Box Office" }
                }
                body { main { (content) } }
            }
        })
    }
}

Composing views

Views are singletons, so a view can depend on another one. The concert page renders inside the layout, shows the poster and the support acts, and links back to the line-up:

pub struct ConcertPageProps<'props> {
    pub concert: ConcertWithVenue,
    pub routes: &'props Routes,
}

#[singleton]
#[renders_view(name = "concert_page")]
pub struct ConcertPage {
    layout: Arc<Layout>,
}

impl ConcertPage {
    #[constructor]
    pub fn create(layout: Arc<Layout>) -> anyhow::Result<Self> {
        Ok(Self { layout })
    }
}

impl RendersView for ConcertPage {
    type Props<'props> = ConcertPageProps<'props>;

    fn render(
        &self,
        ConcertPageProps {
            concert: ConcertWithVenue { concert, venue },
            routes,
        }: Self::Props<'_>,
    ) -> anyhow::Result<Markup> {
        let poster_url = addressed(routes.public.show_poster(concert.id.to_string()))?.url();

        self.layout.render(LayoutProps {
            title: concert.artist.clone(),
            content: html! {
                h1 { (concert.artist) }
                img src=(poster_url) alt="";
                @if !concert.support.payload().is_empty() {
                    p { "With " (concert.support.payload().join(", ")) }
                }
                p { (venue.name) ", " (venue.city) }
                p { (concert.starts_at.format("%A, %B %-d at %H:%M")) }
                p { "Tickets from " (concert.price) " EUR" }
                a href=(routes.public.list_concerts.url()) { "All concerts" }
            },
        })
    }
}

The poster route got a name = "show_poster", so the page can link to it.

Rendering from a route

Named views become fields of the generated Views struct. Ask for &Views in the route:

#[singleton]
#[responds_to_http(
    method = RouteMethod::Get,
    name = "show_concert",
    path = "/concerts/{concert}",
    server = "public"
)]
pub struct ShowConcert;

impl ShowConcert {
    #[process]
    pub fn respond(
        &self,
        routes: &Routes,
        views: &Views,
        #[route_parameter(from = "concert")] concert: ConcertWithVenue,
    ) -> anyhow::Result<Response> {
        Ok(Response::html(
            200,
            views
                .concert_page
                .render(ConcertPageProps { concert, routes })?,
        ))
    }
}

A typo in views.concert_page doesn't compile. The line-up and order pages work exactly the same way: the LineupPage takes the upcoming concerts, and the OrderPage takes an OrderDetails.