diff --git a/src/responders.rs b/src/responders.rs
index 0b40e9e..beebc10 100644
--- a/src/responders.rs
+++ b/src/responders.rs
@@ -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 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 {
- 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 = ::Err;
-
- fn try_from(value: &'a str) -> Result {
- Ok(Self::from_uri(Uri::from_str(value)?))
- }
-}
-
-impl IntoResponseParts for HxLocation {
- type Error = HxError;
-
- fn into_response_parts(self, mut res: ResponseParts) -> Result {
- #[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(
- name: impl ToString,
- data: T,
- ) -> Result {
- 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) -> Result {
- 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::>();
-
- 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 for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTrigger(pub Vec);
-
-impl From for HxResponseTrigger
-where
- T: IntoIterator,
- T::Item: Into,
-{
- 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 {
- 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 for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSettle(pub Vec);
-
-impl From for HxResponseTriggerAfterSettle
-where
- T: IntoIterator,
- T::Item: Into,
-{
- 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 {
- 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 for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSwap(pub Vec);
-
-impl From for HxResponseTriggerAfterSwap
-where
- T: IntoIterator,
- T::Item: Into,
-{
- 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 {
- 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));
- }
-}
diff --git a/src/responders/location.rs b/src/responders/location.rs
new file mode 100644
index 0000000..8ec740b
--- /dev/null
+++ b/src/responders/location.rs
@@ -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 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 {
+ 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 = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self::from_uri(Uri::from_str(value)?))
+ }
+}
+
+impl IntoResponseParts for HxLocation {
+ type Error = HxError;
+
+ fn into_response_parts(self, mut res: ResponseParts) -> Result {
+ #[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,
+ /// An event that "triggered" the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub event: Option,
+ /// A callback that will handle the response HTML.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub handler: Option,
+ /// The target to swap the response into.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub target: Option,
+ /// How the response will be swapped in relative to the target.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub swap: Option,
+ /// Values to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub values: Option,
+ /// Headers to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub headers: Option,
+}
+
+#[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"}"#
+ );
+ }
+}
diff --git a/src/responders/serde.rs b/src/responders/serde.rs
deleted file mode 100644
index 18f997e..0000000
--- a/src/responders/serde.rs
+++ /dev/null
@@ -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,
- /// An event that "triggered" the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event: Option,
- /// A callback that will handle the response HTML.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub handler: Option,
- /// The target to swap the response into.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option,
- /// How the response will be swapped in relative to the target.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub swap: Option,
- /// Values to submit with the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub values: Option,
- /// Headers to submit with the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub headers: Option,
-}
-
-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
- }
-}
diff --git a/src/responders/trigger.rs b/src/responders/trigger.rs
new file mode 100644
index 0000000..647a75c
--- /dev/null
+++ b/src/responders/trigger.rs
@@ -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,
+}
+
+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(
+ name: impl AsRef,
+ data: T,
+ ) -> Result {
+ let data = serde_json::to_value(data)?;
+
+ Ok(Self {
+ name: name.as_ref().to_owned(),
+ #[cfg(feature = "serde")]
+ data: Some(data),
+ })
+ }
+}
+
+impl> From 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) -> Result {
+ let header = events
+ .into_iter()
+ .map(|HxEvent { name }| name)
+ .collect::>()
+ .join(", ");
+ http::HeaderValue::from_str(&header).map_err(Into::into)
+}
+
+#[cfg(feature = "serde")]
+fn events_to_header_value(events: Vec) -> Result {
+ 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::>();
+
+ 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 for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTrigger(pub Vec);
+
+impl From for HxResponseTrigger
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ 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 {
+ 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 for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTriggerAfterSettle(pub Vec);
+
+impl From for HxResponseTriggerAfterSettle
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ 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 {
+ 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 for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTriggerAfterSwap(pub Vec);
+
+impl From for HxResponseTriggerAfterSwap
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ 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 {
+ 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));
+ }
+}