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.