Use HX-Target, not HX-Boost for Layout render

HX-Boost is not always sent in all AJAX requests that htmx sends, only
those initiated by an element with hx-boost enabled. It was not showing
up on requests following an HX-Redirect response.

After reading the docs more, I realized HX-Target was what I wanted. If
I can see that the request is targeting `#main-content` then I know to
only return HTML inside that element. Simple.
This commit is contained in:
2023-09-27 01:28:53 -04:00
parent bea3529e22
commit a72bfa15bd
10 changed files with 110 additions and 60 deletions

View File

@@ -8,7 +8,7 @@ use sqlx::PgPool;
use crate::config::Config;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::htmx::HXTarget;
use crate::models::entry::Entry;
use crate::partials::layout::Layout;
use crate::uuid::Base62Uuid;
@@ -17,7 +17,7 @@ pub async fn get(
Path(id): Path<Base62Uuid>,
State(pool): State<PgPool>,
State(config): State<Config>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
) -> Result<Response> {
let entry = Entry::get(&pool, id.as_uuid()).await?;
@@ -30,7 +30,7 @@ pub async fn get(
let content = fs::read_to_string(content_path).unwrap_or_else(|_| "No content".to_string());
Ok(layout
.with_subtitle(&title)
.boosted(hx_boosted)
.targeted(hx_target)
.render(html! {
article {
header {

View File

@@ -16,7 +16,7 @@ use tokio_stream::StreamExt;
use crate::actors::crawl_scheduler::{CrawlSchedulerHandle, CrawlSchedulerHandleMessage};
use crate::actors::feed_crawler::FeedCrawlerHandleMessage;
use crate::error::{Error, Result};
use crate::htmx::HXBoosted;
use crate::htmx::HXTarget;
use crate::models::entry::{Entry, GetEntriesOptions};
use crate::models::feed::{CreateFeed, Feed};
use crate::partials::add_feed_form::add_feed_form;
@@ -28,7 +28,7 @@ use crate::uuid::Base62Uuid;
pub async fn get(
Path(id): Path<Base62Uuid>,
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
) -> Result<Response> {
let feed = Feed::get(&pool, id.as_uuid()).await?;
@@ -39,7 +39,7 @@ pub async fn get(
let title = feed.title.unwrap_or_else(|| "Untitled Feed".to_string());
let entries = Entry::get_all(&pool, &options).await?;
let delete_url = format!("/feed/{}/delete", id);
Ok(layout.with_subtitle(&title).boosted(hx_boosted).render(html! {
Ok(layout.with_subtitle(&title).targeted(hx_target).render(html! {
header class="feed-header" {
h2 { (title) }
button class="edit-feed" { "✏️ Edit feed" }

View File

@@ -5,7 +5,7 @@ use maud::html;
use sqlx::PgPool;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::htmx::HXTarget;
use crate::models::feed::{Feed, GetFeedsOptions};
use crate::partials::add_feed_form::add_feed_form;
use crate::partials::feed_list::feed_list;
@@ -14,14 +14,14 @@ use crate::partials::opml_import_form::opml_import_form;
pub async fn get(
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
) -> Result<Response> {
let options = GetFeedsOptions::default();
let feeds = Feed::get_all(&pool, &options).await?;
Ok(layout
.with_subtitle("feeds")
.boosted(hx_boosted)
.targeted(hx_target)
.render(html! {
header { h2 { "Feeds" } }
div class="feeds" {

View File

@@ -5,18 +5,18 @@ use maud::html;
use sqlx::PgPool;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::htmx::HXTarget;
use crate::models::entry::Entry;
use crate::partials::{entry_list::entry_list, layout::Layout};
pub async fn get(
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
) -> Result<Response> {
let options = Default::default();
let entries = Entry::get_all(&pool, &options).await?;
Ok(layout.boosted(hx_boosted).render(html! {
Ok(layout.targeted(hx_target).render(html! {
ul class="entries" {
(entry_list(entries, &options))
}

View File

@@ -18,15 +18,15 @@ use tokio_stream::Stream;
use tokio_stream::StreamExt;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::htmx::HXTarget;
use crate::log::MEM_LOG;
use crate::partials::layout::Layout;
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> {
let mem_buf = MEM_LOG.lock().unwrap();
Ok(layout
.with_subtitle("log")
.boosted(hx_boosted)
.targeted(hx_target)
.render(html! {
pre id="log" hx-sse="connect:/log/stream swap:message" hx-swap="beforeend" hx-target="#log" {
(PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap()))

View File

@@ -8,7 +8,7 @@ use sqlx::PgPool;
use crate::auth::verify_password;
use crate::error::{Error, Result};
use crate::htmx::{HXBoosted, HXRedirect};
use crate::htmx::{HXRedirect, HXTarget};
use crate::partials::login_form::{login_form, LoginFormProps};
use crate::{
models::user::{AuthContext, User},
@@ -22,36 +22,49 @@ pub struct Login {
password: String,
}
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
Ok(layout
pub fn login_page(
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
form_props: LoginFormProps,
) -> Response {
layout
.with_subtitle("login")
.boosted(hx_boosted)
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Login" }
}
(login_form(LoginFormProps::default()))
(login_form(form_props))
}
}))
})
.into_response()
}
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> {
Ok(login_page(hx_target, layout, LoginFormProps::default()))
}
pub async fn post(
State(pool): State<PgPool>,
mut auth: AuthContext,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
Form(login): Form<Login>,
) -> Result<Response> {
let user: User = match User::get_by_email(&pool, login.email.clone()).await {
Ok(user) => user,
Err(err) => {
if let Error::NotFoundString(_, _) = err {
// Error::BadRequest("invalid email or password")
return Ok(login_form(LoginFormProps {
email: Some(login.email),
general_error: Some("invalid email or password".to_string()),
..Default::default()
})
.into_response());
return Ok(login_page(
hx_target,
layout,
LoginFormProps {
email: Some(login.email),
general_error: Some("invalid email or password".to_string()),
..Default::default()
},
));
} else {
return Err(err);
}
@@ -61,13 +74,15 @@ pub async fn post(
.await
.is_err()
{
// return Err(Error::BadRequest("invalid email or password"));
return Ok(login_form(LoginFormProps {
email: Some(login.email),
general_error: Some("invalid email or password".to_string()),
..Default::default()
})
.into_response());
return Ok(login_page(
hx_target,
layout,
LoginFormProps {
email: Some(login.email),
general_error: Some("invalid email or password".to_string()),
..Default::default()
},
));
}
auth.login(&user)
.await

View File

@@ -7,7 +7,7 @@ use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool;
use crate::error::{Error, Result};
use crate::htmx::{HXBoosted, HXRedirect};
use crate::htmx::{HXTarget, HXRedirect};
use crate::models::user::{AuthContext, CreateUser, User};
use crate::partials::layout::Layout;
use crate::partials::register_form::{register_form, RegisterFormProps};
@@ -22,10 +22,10 @@ pub struct Register {
pub name: Option<String>,
}
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> {
Ok(layout
.with_subtitle("register")
.boosted(hx_boosted)
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {