feat: overhaul templating system
							parent
							
								
									03b3f692e1
								
							
						
					
					
						commit
						4a4b8bba3d
					
				| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# Calathea
 | 
			
		||||
 | 
			
		||||
## Templates and rendering
 | 
			
		||||
 | 
			
		||||
Calathea uses the [Tera](https://keats.github.io/tera/) templating engine. To
 | 
			
		||||
combine Tera and HTMX, the template directory has the following structure:
 | 
			
		||||
 | 
			
		||||
* `components` contains components used to build views. Components are defined
 | 
			
		||||
  as Tera [macros](https://keats.github.io/tera/docs/#macros).
 | 
			
		||||
* `views` contains view templates that make use of the components.
 | 
			
		||||
* `updates` are templates returned from e.g. POST requests. Tbey usually
 | 
			
		||||
  directly wrap a component, e.g. a plant `li` item.
 | 
			
		||||
* `base.html` defines the skeleton for each page. Each view is rendered with
 | 
			
		||||
  this as its surrounding HTML.
 | 
			
		||||
 | 
			
		||||
When a page is requested from the server, it inspects the `HX-Request` and
 | 
			
		||||
`HX-History-Restore-Request` headers to determine whether a full page should be
 | 
			
		||||
returned or only the inner view. Using this, the server cn support direct
 | 
			
		||||
routing to pages and caching properly while still supporting partial rendering
 | 
			
		||||
using HTMX.
 | 
			
		||||
							
								
								
									
										39
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										39
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
				
			
			@ -32,7 +32,7 @@ async fn main() {
 | 
			
		|||
    let pool = r2d2::Pool::new(manager).unwrap();
 | 
			
		||||
    db::run_migrations(&pool, &MIGRATIONS).unwrap();
 | 
			
		||||
 | 
			
		||||
    let tera = load_templates();
 | 
			
		||||
    let tera = Tera::new("templates/**/*").unwrap();
 | 
			
		||||
    let ctx = Context {
 | 
			
		||||
        pool,
 | 
			
		||||
        tera: Arc::new(tera),
 | 
			
		||||
| 
						 | 
				
			
			@ -48,40 +48,3 @@ async fn main() {
 | 
			
		|||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn load_templates() -> Tera {
 | 
			
		||||
    let mut tera = Tera::default();
 | 
			
		||||
 | 
			
		||||
    tera.add_raw_templates(vec![
 | 
			
		||||
        ("base.html", include_str!("templates/base.html")),
 | 
			
		||||
        ("index.html", include_str!("templates/index.html")),
 | 
			
		||||
        (
 | 
			
		||||
            "partials/plants_ul.html",
 | 
			
		||||
            include_str!("templates/partials/plants_ul.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "partials/plant_li.html",
 | 
			
		||||
            include_str!("templates/partials/plant_li.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        ("plant_page.html", include_str!("templates/plant_page.html")),
 | 
			
		||||
        (
 | 
			
		||||
            "partials/plant_info.html",
 | 
			
		||||
            include_str!("templates/partials/plant_info.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "partials/comment_li.html",
 | 
			
		||||
            include_str!("templates/partials/comment_li.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "partials/event_li.html",
 | 
			
		||||
            include_str!("templates/partials/event_li.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "macros/event.html",
 | 
			
		||||
            include_str!("templates/macros/event.html"),
 | 
			
		||||
        ),
 | 
			
		||||
    ])
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    tera
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,5 +17,5 @@ async fn post_comment(
 | 
			
		|||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("comment", &comment);
 | 
			
		||||
    Ok(Html(ctx.tera.render("partials/comment_li.html", &context)?))
 | 
			
		||||
    Ok(Html(ctx.tera.render("updates/comment_li.html", &context)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,5 +17,5 @@ async fn post_event(
 | 
			
		|||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("event", &event);
 | 
			
		||||
    Ok(Html(ctx.tera.render("partials/event_li.html", &context)?))
 | 
			
		||||
    Ok(Html(ctx.tera.render("updates/event_li.html", &context)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,14 +20,32 @@ pub type Result<T> = std::result::Result<T, error::AppError>;
 | 
			
		|||
const HX_REQUEST_HEADER: &str = "HX-Request";
 | 
			
		||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
 | 
			
		||||
 | 
			
		||||
pub fn render_partial(headers: &HeaderMap) -> bool {
 | 
			
		||||
pub fn should_render_full(headers: &HeaderMap) -> bool {
 | 
			
		||||
    let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
 | 
			
		||||
    let is_hist_restore_req = headers
 | 
			
		||||
        .get(HX_HISTORY_RESTORE_HEADER)
 | 
			
		||||
        .map(|val| val == HeaderValue::from_static("true"))
 | 
			
		||||
        .unwrap_or(false);
 | 
			
		||||
 | 
			
		||||
    is_htmx_req && !is_hist_restore_req
 | 
			
		||||
    !is_htmx_req || is_hist_restore_req
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn render_view(
 | 
			
		||||
    tera: &tera::Tera,
 | 
			
		||||
    view: &str,
 | 
			
		||||
    ctx: &tera::Context,
 | 
			
		||||
    headers: &HeaderMap,
 | 
			
		||||
) -> tera::Result<String> {
 | 
			
		||||
    let view = tera.render(view, ctx)?;
 | 
			
		||||
 | 
			
		||||
    if should_render_full(headers) {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("view", &view);
 | 
			
		||||
 | 
			
		||||
        tera.render("base.html", &ctx)
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok(view)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn app(ctx: crate::Context) -> axum::Router {
 | 
			
		||||
| 
						 | 
				
			
			@ -52,12 +70,17 @@ pub fn app(ctx: crate::Context) -> axum::Router {
 | 
			
		|||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_index(State(ctx): State<crate::Context>) -> Result<Html<String>> {
 | 
			
		||||
async fn get_index(State(ctx): State<crate::Context>, headers: HeaderMap) -> Result<Html<String>> {
 | 
			
		||||
    let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool))
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap()?;
 | 
			
		||||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("plants", &plants);
 | 
			
		||||
    Ok(Html(ctx.tera.render("index.html", &context)?))
 | 
			
		||||
    Ok(Html(render_view(
 | 
			
		||||
        &ctx.tera,
 | 
			
		||||
        "views/index.html",
 | 
			
		||||
        &context,
 | 
			
		||||
        &headers,
 | 
			
		||||
    )?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ use tera::Context;
 | 
			
		|||
 | 
			
		||||
use crate::db::{self, DbError, Plant};
 | 
			
		||||
 | 
			
		||||
use super::{error::AppError, render_partial};
 | 
			
		||||
use super::error::AppError;
 | 
			
		||||
 | 
			
		||||
pub fn app() -> axum::Router<crate::Context> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
| 
						 | 
				
			
			@ -45,13 +45,12 @@ async fn get_plant_page(
 | 
			
		|||
            context.insert("events", &events);
 | 
			
		||||
            context.insert("event_types", &db::EVENT_TYPES);
 | 
			
		||||
 | 
			
		||||
            let tmpl = if render_partial(&headers) {
 | 
			
		||||
                "partials/plant_info.html"
 | 
			
		||||
            } else {
 | 
			
		||||
                "plant_page.html"
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            Ok(Html(ctx.tera.render(tmpl, &context)?))
 | 
			
		||||
            Ok(Html(super::render_view(
 | 
			
		||||
                &ctx.tera,
 | 
			
		||||
                "views/plant.html",
 | 
			
		||||
                &context,
 | 
			
		||||
                &headers,
 | 
			
		||||
            )?))
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(AppError::NotFound),
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -67,5 +66,5 @@ async fn post_plant(
 | 
			
		|||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("plant", &plant);
 | 
			
		||||
    Ok(Html(ctx.tera.render("partials/plant_li.html", &context)?))
 | 
			
		||||
    Ok(Html(ctx.tera.render("updates/plant_li.html", &context)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
<h1>Calathea</h1>
 | 
			
		||||
<h2>Plants</h2>
 | 
			
		||||
{% include "partials/plants_ul.html" %}
 | 
			
		||||
<h3>Add new plant</h3>
 | 
			
		||||
<form hx-post="/plants" hx-target="#plants" hx-swap="beforeend">
 | 
			
		||||
    <label for="name">Name:</label>
 | 
			
		||||
    <input type="text" id="name" name="name"></br>
 | 
			
		||||
    <label for="species">Species:</label>
 | 
			
		||||
    <input type="text" id="species" name="species"></br>
 | 
			
		||||
    <label for="description">Description:</label>
 | 
			
		||||
    <textarea id="description" name="description" rows=4></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<li>{{ comment.comment }}</li>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
{{ macros_event::li(event=event) }}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
 | 
			
		||||
<h1>Calathea</h1>
 | 
			
		||||
<h2>{{ plant.name }}</h2>
 | 
			
		||||
<h3>Species</h3>
 | 
			
		||||
<p>{{ plant.species }}</p>
 | 
			
		||||
<h3>Description</h3>
 | 
			
		||||
<p>{{ plant.description }}</p>
 | 
			
		||||
<h3>Events</h3>
 | 
			
		||||
<div id="events">
 | 
			
		||||
    <ul id="events_ul">
 | 
			
		||||
        {% for event in events %}
 | 
			
		||||
            {{ macros_event::li(event=event) }}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
    {{ macros_event::form(plant_id=plant.id) }}
 | 
			
		||||
</div>
 | 
			
		||||
<h3>Comments</h3>
 | 
			
		||||
<ul id="comments">
 | 
			
		||||
{% for comment in comments %}
 | 
			
		||||
    <li>
 | 
			
		||||
        {{ comment.comment }}
 | 
			
		||||
    </li>
 | 
			
		||||
{% endfor %}
 | 
			
		||||
</ul>
 | 
			
		||||
<form hx-post="/comments" hx-target="#comments" hx-swap="beforeend">
 | 
			
		||||
    <input type="hidden" id="plant_id" name="plant_id" value="{{ plant.id }}">
 | 
			
		||||
    <label for="comment">Comment:</label>
 | 
			
		||||
    <textarea id="comment" name="comment" rows=4></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<li>{{ plant.name }} ({{ plant.species }})</li>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
<ul id="plants">
 | 
			
		||||
    {% for plant in plants %}
 | 
			
		||||
    <li>
 | 
			
		||||
        <a hx-get="/plants/{{ plant.id }}" hx-target="#content" hx-push-url="true">{{ plant.name }}</a> ({{ plant.species }})
 | 
			
		||||
    </li>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
</ul>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% include "partials/plant_info.html" %}
 | 
			
		||||
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,9 +4,11 @@
 | 
			
		|||
        <script src="/static/htmx_2.0.4.min.js" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <main>
 | 
			
		||||
            <h1>Calathea</h1>
 | 
			
		||||
            <div id="content">
 | 
			
		||||
            {% block content %}
 | 
			
		||||
            {% endblock content %}
 | 
			
		||||
                {{ view | safe }}
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
{% macro li(comment) %}
 | 
			
		||||
<li>{{ comment.comment }}</li>
 | 
			
		||||
{% endmacro li %}
 | 
			
		||||
 | 
			
		||||
{% macro list(comments) %}
 | 
			
		||||
<div id="comments">
 | 
			
		||||
    <ul id="comments_ul">
 | 
			
		||||
    {% for comment in comments %}
 | 
			
		||||
        {{ self::li(comment=comment) }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
{% endmacro list %}
 | 
			
		||||
 | 
			
		||||
{% macro form(plant_id, target="#comments > ul") %}
 | 
			
		||||
<form hx-post="/comments" hx-target="{{ target }}" hx-swap="beforeend">
 | 
			
		||||
    <input type="hidden" id="plant_id" name="plant_id" value="{{ plant_id }}">
 | 
			
		||||
    <label for="comment">Comment:</label>
 | 
			
		||||
    <textarea id="comment" name="comment" rows=4></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
{% endmacro form %}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,17 @@
 | 
			
		|||
</li>
 | 
			
		||||
{% endmacro li %}
 | 
			
		||||
 | 
			
		||||
{% macro form(plant_id, target="#events_ul") %}
 | 
			
		||||
{% macro list(events) %}
 | 
			
		||||
<div id="events">
 | 
			
		||||
    <ul>
 | 
			
		||||
    {% for event in events %}
 | 
			
		||||
        {{ self::li(event=event) }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
{% endmacro ul %}
 | 
			
		||||
 | 
			
		||||
{% macro form(plant_id, target="#events > ul") %}
 | 
			
		||||
<form hx-post="/events" hx-target="{{ target }}" hx-swap="beforeend">
 | 
			
		||||
    <input type="hidden" id="plant_id" name="plant_id" value="{{ plant_id }}">
 | 
			
		||||
    <label for="event_type">Type:</label>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
{% macro info(plant) %}
 | 
			
		||||
<div class="plant_info">
 | 
			
		||||
    <h2>{{ plant.name }}</h2>
 | 
			
		||||
    <h3>Species</h3>
 | 
			
		||||
    <p>{{ plant.species }}</p>
 | 
			
		||||
    <h3>Description</h3>
 | 
			
		||||
    <p>{{ plant.description }}</p>
 | 
			
		||||
</div>
 | 
			
		||||
{% endmacro info %}
 | 
			
		||||
 | 
			
		||||
{% macro li(plant) %}
 | 
			
		||||
<li>
 | 
			
		||||
    <a hx-get="/plants/{{ plant.id }}" hx-target="#content" hx-push-url="true">{{ plant.name }}</a> ({{ plant.species }})
 | 
			
		||||
</li>
 | 
			
		||||
{% endmacro li %}
 | 
			
		||||
 | 
			
		||||
{% macro list(plants) %}
 | 
			
		||||
<div id="plants">
 | 
			
		||||
    <ul>
 | 
			
		||||
    {% for plant in plants %}
 | 
			
		||||
        {{ self::li(plant=plant) }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
 | 
			
		||||
{% macro form(target="#plants > ul") %}
 | 
			
		||||
<form hx-post="/plants" hx-target="{{ target }}" hx-swap="beforeend">
 | 
			
		||||
    <label for="name">Name:</label>
 | 
			
		||||
    <input type="text" id="name" name="name"></br>
 | 
			
		||||
    <label for="species">Species:</label>
 | 
			
		||||
    <input type="text" id="species" name="species"></br>
 | 
			
		||||
    <label for="description">Description:</label>
 | 
			
		||||
    <textarea id="description" name="description" rows=4></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{% import "components/comment.html" as comp_comment %}
 | 
			
		||||
{{ comp_comment::li(comment=comment) }}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{% import "components/event.html" as comp_event %}
 | 
			
		||||
{{ comp_event::li(event=event) }}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{% import "components/plant.html" as comp_plant %}
 | 
			
		||||
{{ comp_plant::li(plant=plant) }}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
{% import "components/plant.html" as comp_plant %}
 | 
			
		||||
 | 
			
		||||
<h2>Plants</h2>
 | 
			
		||||
{{ comp_plant::list(plants=plants) }}
 | 
			
		||||
<h3>Add new plant</h3>
 | 
			
		||||
{{ comp_plant::form() }}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{% import "components/event.html" as comp_event %}
 | 
			
		||||
{% import "components/plant.html" as comp_plant %}
 | 
			
		||||
{% import "components/comment.html" as comp_comment %}
 | 
			
		||||
 | 
			
		||||
{{ comp_plant::info(plant=plant) }}
 | 
			
		||||
<h3>Events</h3>
 | 
			
		||||
{{ comp_event::list(events=events) }}
 | 
			
		||||
{{ comp_event::form(plant_id=plant.id) }}
 | 
			
		||||
<h3>Comments</h3>
 | 
			
		||||
{{ comp_comment::list(comments=comments) }}
 | 
			
		||||
{{ comp_comment::form(plant_id=plant.id) }}
 | 
			
		||||
		Loading…
	
		Reference in New Issue