mirror of
https://github.com/robertwayne/axum-htmx
synced 2024-12-28 03:09:32 +01:00
Moved HxLocation && HxTrigger* into different modules
This commit is contained in:
parent
a223399361
commit
1dfff29673
4 changed files with 393 additions and 361 deletions
|
@ -7,9 +7,10 @@ use http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri};
|
|||
|
||||
use crate::headers;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub mod serde;
|
||||
mod location;
|
||||
pub use location::*;
|
||||
mod trigger;
|
||||
pub use trigger::*;
|
||||
|
||||
const HX_SWAP_INNER_HTML: &str = "innerHTML";
|
||||
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
|
||||
|
@ -20,89 +21,6 @@ const HX_SWAP_AFTER_END: &str = "afterend";
|
|||
const HX_SWAP_DELETE: &str = "delete";
|
||||
const HX_SWAP_NONE: &str = "none";
|
||||
|
||||
/// The `HX-Location` header.
|
||||
///
|
||||
/// This response header can be used to trigger a client side redirection
|
||||
/// without reloading the whole page. If you intend to redirect to a specific
|
||||
/// target on the page, you must enable the `serde` feature flag and use
|
||||
/// `axum_htmx::responders::serde::HxLocation` instead.
|
||||
///
|
||||
/// Will fail if the supplied Uri contains characters that are not visible ASCII
|
||||
/// (32-127).
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-location/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxLocation {
|
||||
/// Uri of the new location.
|
||||
pub uri: Uri,
|
||||
/// Extra options.
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub options: serde::LocationOptions,
|
||||
}
|
||||
|
||||
impl HxLocation {
|
||||
pub fn from_uri(uri: Uri) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "serde")]
|
||||
options: serde::LocationOptions::default(),
|
||||
uri,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub fn from_uri_with_options(uri: Uri, options: serde::LocationOptions) -> Self {
|
||||
Self { uri, options }
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn into_header_with_options(self) -> Result<String, HxError> {
|
||||
if self.options.is_default() {
|
||||
return Ok(self.uri.to_string());
|
||||
}
|
||||
|
||||
#[derive(::serde::Serialize)]
|
||||
struct LocWithOpts {
|
||||
path: String,
|
||||
#[serde(flatten)]
|
||||
opts: serde::LocationOptions,
|
||||
}
|
||||
|
||||
let loc_with_opts = LocWithOpts {
|
||||
path: self.uri.to_string(),
|
||||
opts: self.options,
|
||||
};
|
||||
Ok(serde_json::to_string(&loc_with_opts)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for HxLocation {
|
||||
type Error = <Uri as FromStr>::Err;
|
||||
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
Ok(Self::from_uri(Uri::from_str(value)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxLocation {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
#[cfg(feature = "serde")]
|
||||
let header = self.into_header_with_options()?;
|
||||
#[cfg(not(feature = "serde"))]
|
||||
let header = self.uri.to_string();
|
||||
|
||||
res.headers_mut().insert(
|
||||
headers::HX_LOCATION,
|
||||
HeaderValue::from_maybe_shared(header)?,
|
||||
);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `HX-Push-Url` header.
|
||||
///
|
||||
/// Pushes a new url into the history stack.
|
||||
|
@ -286,174 +204,6 @@ impl IntoResponseParts for HxReselect {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents a client-side event carrying optional data.
|
||||
#[derive(Debug, Clone, ::serde::Serialize)]
|
||||
pub struct HxEvent {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub data: Option<::serde_json::Value>,
|
||||
}
|
||||
|
||||
impl HxEvent {
|
||||
/// Creates new event with no associated data.
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new event with event data.
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub fn new_with_data<T: ::serde::Serialize>(
|
||||
name: impl ToString,
|
||||
data: T,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
let data = serde_json::to_value(data)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
data: Some(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn events_to_header_value(events: Vec<HxEvent>) -> Result<HeaderValue, HxError> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
let with_data = events.iter().any(|e| e.data.is_some());
|
||||
|
||||
let header_value = if with_data {
|
||||
// at least one event contains data so the header_value needs to be json
|
||||
// encoded.
|
||||
let header_value = events
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.data.unwrap_or_default()))
|
||||
.collect::<HashMap<String, Value>>();
|
||||
|
||||
serde_json::to_string(&header_value)?
|
||||
} else {
|
||||
// no event contains data, the event names can be put in the header
|
||||
// value separated by a comma.
|
||||
events
|
||||
.into_iter()
|
||||
.map(|e| e.name)
|
||||
.reduce(|acc, e| acc + ", " + &e)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
|
||||
}
|
||||
|
||||
/// The `HX-Trigger` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events. If you intend to add data to your
|
||||
/// events, you must enable the `serde` feature flag and use
|
||||
/// `axum_htmx::responders::serde::HxResponseTrigger` instead.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTrigger(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTrigger
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTrigger {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `HX-Trigger-After-Settle` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events after the settle step. If you
|
||||
/// intend to add data to your events, you must enable the `serde` feature flag
|
||||
/// and use `axum_htmx::responders::serde::HxResponseTriggerAfterSettle`
|
||||
/// instead.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTriggerAfterSettle(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTriggerAfterSettle
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTriggerAfterSettle {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `HX-Trigger-After-Swap` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events after the swap step. If you intend
|
||||
/// to add data to your events, you must enable the `serde` feature flag and use
|
||||
/// `axum_htmx::responders::serde::HxResponseTriggerAfterSwap` instead.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTriggerAfterSwap(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTriggerAfterSwap
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTriggerAfterSwap {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Values of the `hx-swap` attribute.
|
||||
// serde::Serialize is implemented in responders/serde.rs
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
|
@ -557,51 +307,3 @@ impl IntoResponse for HxError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::HeaderValue;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{responders::events_to_header_value, HxEvent};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "serde")]
|
||||
fn test_serialize_location() {
|
||||
use crate::{serde::LocationOptions, HxLocation, SwapOption::InnerHtml};
|
||||
|
||||
let loc = HxLocation::try_from("/foo").unwrap();
|
||||
assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
|
||||
|
||||
let loc = HxLocation::from_uri_with_options(
|
||||
"/foo".parse().unwrap(),
|
||||
LocationOptions {
|
||||
event: Some("click".into()),
|
||||
swap: Some(InnerHtml),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
loc.into_header_with_options().unwrap(),
|
||||
r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_event_to_header_encoding() {
|
||||
let evt = HxEvent::new_with_data(
|
||||
"my-event",
|
||||
json!({"level": "info", "message": {
|
||||
"body": "This is a test message.",
|
||||
"title": "Hello, world!",
|
||||
}}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header_value = events_to_header_value(vec![evt]).unwrap();
|
||||
|
||||
let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
|
||||
|
||||
assert_eq!(header_value, HeaderValue::from_static(expected_value));
|
||||
}
|
||||
}
|
||||
|
|
175
src/responders/location.rs
Normal file
175
src/responders/location.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use axum_core::response::{IntoResponseParts, ResponseParts};
|
||||
use http::{HeaderValue, Uri};
|
||||
|
||||
use crate::{headers, HxError};
|
||||
|
||||
/// The `HX-Location` header.
|
||||
///
|
||||
/// This response header can be used to trigger a client side redirection
|
||||
/// without reloading the whole page. If you intend to redirect to a specific
|
||||
/// target on the page, you must enable the `serde` feature flag and specify
|
||||
/// [`LocationOptions`].
|
||||
///
|
||||
/// Will fail if the supplied Uri contains characters that are not visible ASCII
|
||||
/// (32-127).
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-location/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxLocation {
|
||||
/// Uri of the new location.
|
||||
pub uri: Uri,
|
||||
/// Extra options.
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub options: LocationOptions,
|
||||
}
|
||||
|
||||
impl HxLocation {
|
||||
pub fn from_uri(uri: Uri) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "serde")]
|
||||
options: LocationOptions::default(),
|
||||
uri,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub fn from_uri_with_options(uri: Uri, options: LocationOptions) -> Self {
|
||||
Self { uri, options }
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn into_header_with_options(self) -> Result<String, HxError> {
|
||||
if self.options.is_default() {
|
||||
return Ok(self.uri.to_string());
|
||||
}
|
||||
|
||||
#[derive(::serde::Serialize)]
|
||||
struct LocWithOpts {
|
||||
path: String,
|
||||
#[serde(flatten)]
|
||||
opts: LocationOptions,
|
||||
}
|
||||
|
||||
let loc_with_opts = LocWithOpts {
|
||||
path: self.uri.to_string(),
|
||||
opts: self.options,
|
||||
};
|
||||
Ok(serde_json::to_string(&loc_with_opts)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for HxLocation {
|
||||
type Error = <Uri as FromStr>::Err;
|
||||
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
Ok(Self::from_uri(Uri::from_str(value)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxLocation {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
#[cfg(feature = "serde")]
|
||||
let header = self.into_header_with_options()?;
|
||||
#[cfg(not(feature = "serde"))]
|
||||
let header = self.uri.to_string();
|
||||
|
||||
res.headers_mut().insert(
|
||||
headers::HX_LOCATION,
|
||||
HeaderValue::from_maybe_shared(header)?,
|
||||
);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// More options for `HX-Location` header.
|
||||
///
|
||||
/// - `source` - the source element of the request
|
||||
/// - `event` - an event that “triggered” the request
|
||||
/// - `handler` - a callback that will handle the response HTML
|
||||
/// - `target` - the target to swap the response into
|
||||
/// - `swap` - how the response will be swapped in relative to the target
|
||||
/// - `values` - values to submit with the request
|
||||
/// - `headers` - headers to submit with the request
|
||||
/// - `select` - allows you to select the content you want swapped from a response
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
#[derive(Debug, Clone, serde::Serialize, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct LocationOptions {
|
||||
/// The source element of the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<String>,
|
||||
/// An event that "triggered" the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event: Option<String>,
|
||||
/// A callback that will handle the response HTML.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub handler: Option<String>,
|
||||
/// The target to swap the response into.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target: Option<String>,
|
||||
/// How the response will be swapped in relative to the target.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub swap: Option<crate::SwapOption>,
|
||||
/// Values to submit with the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<serde_json::Value>,
|
||||
/// Headers to submit with the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub headers: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
impl LocationOptions {
|
||||
pub(super) fn is_default(&self) -> bool {
|
||||
let Self {
|
||||
source: None,
|
||||
event: None,
|
||||
handler: None,
|
||||
target: None,
|
||||
swap: None,
|
||||
values: None,
|
||||
headers: None,
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "serde")]
|
||||
fn test_serialize_location() {
|
||||
use crate::SwapOption;
|
||||
|
||||
let loc = HxLocation::try_from("/foo").unwrap();
|
||||
assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
|
||||
|
||||
let loc = HxLocation::from_uri_with_options(
|
||||
"/foo".parse().unwrap(),
|
||||
LocationOptions {
|
||||
event: Some("click".into()),
|
||||
swap: Some(SwapOption::InnerHtml),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
loc.into_header_with_options().unwrap(),
|
||||
r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::SwapOption;
|
||||
|
||||
/// More options for `HX-Location` header.
|
||||
///
|
||||
/// - `source` - the source element of the request
|
||||
/// - `event` - an event that “triggered” the request
|
||||
/// - `handler` - a callback that will handle the response HTML
|
||||
/// - `target` - the target to swap the response into
|
||||
/// - `swap` - how the response will be swapped in relative to the target
|
||||
/// - `values` - values to submit with the request
|
||||
/// - `headers` - headers to submit with the request
|
||||
/// - `select` - allows you to select the content you want swapped from a response
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct LocationOptions {
|
||||
/// The source element of the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<String>,
|
||||
/// An event that "triggered" the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event: Option<String>,
|
||||
/// A callback that will handle the response HTML.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub handler: Option<String>,
|
||||
/// The target to swap the response into.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target: Option<String>,
|
||||
/// How the response will be swapped in relative to the target.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub swap: Option<SwapOption>,
|
||||
/// Values to submit with the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<Value>,
|
||||
/// Headers to submit with the request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub headers: Option<Value>,
|
||||
}
|
||||
|
||||
impl LocationOptions {
|
||||
pub(super) fn is_default(&self) -> bool {
|
||||
let Self {
|
||||
source: None,
|
||||
event: None,
|
||||
handler: None,
|
||||
target: None,
|
||||
swap: None,
|
||||
values: None,
|
||||
headers: None,
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
}
|
214
src/responders/trigger.rs
Normal file
214
src/responders/trigger.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use axum_core::response::{IntoResponseParts, ResponseParts};
|
||||
|
||||
use crate::{headers, HxError};
|
||||
|
||||
/// Represents a client-side event carrying optional data.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct HxEvent {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl HxEvent {
|
||||
/// Creates new event with no associated data.
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
#[cfg(feature = "serde")]
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new event with event data.
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||
pub fn new_with_data<T: ::serde::Serialize>(
|
||||
name: impl AsRef<str>,
|
||||
data: T,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
let data = serde_json::to_value(data)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.as_ref().to_owned(),
|
||||
#[cfg(feature = "serde")]
|
||||
data: Some(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: AsRef<str>> From<N> for HxEvent {
|
||||
fn from(name: N) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().to_owned(),
|
||||
#[cfg(feature = "serde")]
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serde"))]
|
||||
fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
|
||||
let header = events
|
||||
.into_iter()
|
||||
.map(|HxEvent { name }| name)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
http::HeaderValue::from_str(&header).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http::HeaderValue;
|
||||
use serde_json::Value;
|
||||
|
||||
let with_data = events.iter().any(|e| e.data.is_some());
|
||||
|
||||
let header_value = if with_data {
|
||||
// at least one event contains data so the header_value needs to be json
|
||||
// encoded.
|
||||
let header_value = events
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.data.unwrap_or_default()))
|
||||
.collect::<HashMap<String, Value>>();
|
||||
|
||||
serde_json::to_string(&header_value)?
|
||||
} else {
|
||||
// no event contains data, the event names can be put in the header
|
||||
// value separated by a comma.
|
||||
events
|
||||
.into_iter()
|
||||
.map(|e| e.name)
|
||||
.reduce(|acc, e| acc + ", " + &e)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
|
||||
}
|
||||
|
||||
/// The `HX-Trigger` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTrigger(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTrigger
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTrigger {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `HX-Trigger-After-Settle` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events after the settle step.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTriggerAfterSettle(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTriggerAfterSettle
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTriggerAfterSettle {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `HX-Trigger-After-Swap` header.
|
||||
///
|
||||
/// Allows you to trigger client-side events after the swap step.
|
||||
///
|
||||
/// Will fail if the supplied events contain or produce characters that are not
|
||||
/// visible ASCII (32-127) when serializing to JSON.
|
||||
///
|
||||
/// See <https://htmx.org/headers/hx-trigger/> for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HxResponseTriggerAfterSwap(pub Vec<HxEvent>);
|
||||
|
||||
impl<T> From<T> for HxResponseTriggerAfterSwap
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<HxEvent>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for HxResponseTriggerAfterSwap {
|
||||
type Error = HxError;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::HeaderValue;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid_event_to_header_encoding() {
|
||||
let evt = HxEvent::new_with_data(
|
||||
"my-event",
|
||||
json!({"level": "info", "message": {
|
||||
"body": "This is a test message.",
|
||||
"title": "Hello, world!",
|
||||
}}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header_value = events_to_header_value(vec![evt]).unwrap();
|
||||
|
||||
let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
|
||||
|
||||
assert_eq!(header_value, HeaderValue::from_static(expected_value));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue