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> | ||||
|         <div id="content"> | ||||
|             {% block content %} | ||||
|             {% endblock content %} | ||||
|         </div> | ||||
|         <main> | ||||
|             <h1>Calathea</h1> | ||||
|             <div id="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