Skip to main content

topcoat_htmx/
response.rs

1use http::HeaderValue;
2use http::response::Parts;
3use topcoat_core::{context::Cx, error::Result};
4use topcoat_router::IntoResponseParts;
5
6use crate::SwapOption;
7use crate::header;
8
9/// Pushes a new URL onto the browser history stack via the `HX-Push-Url`
10/// header.
11///
12/// Construct it from a URL string. Use [`HxPushUrl::prevent`] to send
13/// `HX-Push-Url: false`, which stops htmx from updating history.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct HxPushUrl(pub String);
16
17impl HxPushUrl {
18    /// Sends `HX-Push-Url: false`, preventing htmx from updating the history.
19    #[must_use]
20    pub fn prevent() -> Self {
21        Self("false".to_owned())
22    }
23}
24
25impl<T: Into<String>> From<T> for HxPushUrl {
26    fn from(url: T) -> Self {
27        Self(url.into())
28    }
29}
30
31impl IntoResponseParts for HxPushUrl {
32    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
33        parts
34            .headers
35            .insert(header::HX_PUSH_URL, HeaderValue::from_str(&self.0)?);
36        Ok(())
37    }
38}
39
40/// Replaces the current URL in the location bar via the `HX-Replace-Url`
41/// header.
42///
43/// Use [`HxReplaceUrl::prevent`] to send `HX-Replace-Url: false`, which stops
44/// htmx from updating the location bar.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct HxReplaceUrl(pub String);
47
48impl HxReplaceUrl {
49    /// Sends `HX-Replace-Url: false`, preventing htmx from updating the URL.
50    #[must_use]
51    pub fn prevent() -> Self {
52        Self("false".to_owned())
53    }
54}
55
56impl<T: Into<String>> From<T> for HxReplaceUrl {
57    fn from(url: T) -> Self {
58        Self(url.into())
59    }
60}
61
62impl IntoResponseParts for HxReplaceUrl {
63    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
64        parts
65            .headers
66            .insert(header::HX_REPLACE_URL, HeaderValue::from_str(&self.0)?);
67        Ok(())
68    }
69}
70
71/// Performs a client-side redirect to a new location via the `HX-Redirect`
72/// header.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct HxRedirect(pub String);
75
76impl<T: Into<String>> From<T> for HxRedirect {
77    fn from(url: T) -> Self {
78        Self(url.into())
79    }
80}
81
82impl IntoResponseParts for HxRedirect {
83    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
84        parts
85            .headers
86            .insert(header::HX_REDIRECT, HeaderValue::from_str(&self.0)?);
87        Ok(())
88    }
89}
90
91/// Triggers a full client-side page refresh via the `HX-Refresh` header when
92/// `true`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct HxRefresh(pub bool);
95
96impl From<bool> for HxRefresh {
97    fn from(refresh: bool) -> Self {
98        Self(refresh)
99    }
100}
101
102impl IntoResponseParts for HxRefresh {
103    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
104        let value = if self.0 { "true" } else { "false" };
105        parts
106            .headers
107            .insert(header::HX_REFRESH, HeaderValue::from_static(value));
108        Ok(())
109    }
110}
111
112/// Overrides how the response is swapped in via the `HX-Reswap` header. See
113/// [`SwapOption`].
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct HxReswap(pub SwapOption);
116
117impl From<SwapOption> for HxReswap {
118    fn from(option: SwapOption) -> Self {
119        Self(option)
120    }
121}
122
123impl IntoResponseParts for HxReswap {
124    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
125        parts.headers.insert(header::HX_RESWAP, self.0.into());
126        Ok(())
127    }
128}
129
130/// Retargets the content update to a different element via the `HX-Retarget`
131/// header. The value is a CSS selector.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct HxRetarget(pub String);
134
135impl<T: Into<String>> From<T> for HxRetarget {
136    fn from(selector: T) -> Self {
137        Self(selector.into())
138    }
139}
140
141impl IntoResponseParts for HxRetarget {
142    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
143        parts
144            .headers
145            .insert(header::HX_RETARGET, HeaderValue::from_str(&self.0)?);
146        Ok(())
147    }
148}
149
150/// Chooses which part of the response is swapped in via the `HX-Reselect`
151/// header, overriding an existing `hx-select`. The value is a CSS selector.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct HxReselect(pub String);
154
155impl<T: Into<String>> From<T> for HxReselect {
156    fn from(selector: T) -> Self {
157        Self(selector.into())
158    }
159}
160
161impl IntoResponseParts for HxReselect {
162    fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
163        parts
164            .headers
165            .insert(header::HX_RESELECT, HeaderValue::from_str(&self.0)?);
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn parts() -> Parts {
175        http::Response::new(()).into_parts().0
176    }
177
178    fn header_value(parts: &Parts, name: &http::HeaderName) -> String {
179        parts
180            .headers
181            .get(name)
182            .unwrap()
183            .to_str()
184            .unwrap()
185            .to_owned()
186    }
187
188    #[test]
189    fn push_url_sets_header() {
190        let mut parts = parts();
191        HxPushUrl::from("/new")
192            .into_response_parts(&Cx::default(), &mut parts)
193            .unwrap();
194        assert_eq!(header_value(&parts, &header::HX_PUSH_URL), "/new");
195    }
196
197    #[test]
198    fn push_url_prevent_is_false() {
199        let mut parts = parts();
200        HxPushUrl::prevent()
201            .into_response_parts(&Cx::default(), &mut parts)
202            .unwrap();
203        assert_eq!(header_value(&parts, &header::HX_PUSH_URL), "false");
204    }
205
206    #[test]
207    fn refresh_serializes_bool() {
208        let mut parts = parts();
209        HxRefresh(true)
210            .into_response_parts(&Cx::default(), &mut parts)
211            .unwrap();
212        assert_eq!(header_value(&parts, &header::HX_REFRESH), "true");
213    }
214
215    #[test]
216    fn reswap_uses_swap_option_string() {
217        let mut parts = parts();
218        HxReswap(SwapOption::BeforeEnd)
219            .into_response_parts(&Cx::default(), &mut parts)
220            .unwrap();
221        assert_eq!(header_value(&parts, &header::HX_RESWAP), "beforeend");
222    }
223
224    #[test]
225    fn retarget_and_reselect_carry_selectors() {
226        let mut parts = parts();
227        HxRetarget::from("#main")
228            .into_response_parts(&Cx::default(), &mut parts)
229            .unwrap();
230        HxReselect::from(".item")
231            .into_response_parts(&Cx::default(), &mut parts)
232            .unwrap();
233        assert_eq!(header_value(&parts, &header::HX_RETARGET), "#main");
234        assert_eq!(header_value(&parts, &header::HX_RESELECT), ".item");
235    }
236}