actix_htmx/
location.rs

1use serde::Serialize;
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5use crate::SwapType;
6
7/// Builder for `HX-Location` header bodies.
8///
9/// HX-Location lets you instruct htmx to perform a navigation without a full
10/// page reload while still providing extra context (target selector, swap mode,
11/// request headers, etc.). Use [`Htmx::redirect_with_location`](crate::Htmx::redirect_with_location)
12/// to send the resulting header.
13#[derive(Clone, Debug, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct HxLocation {
16    path: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    target: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    source: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    event: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    swap: Option<String>,
25    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
26    headers: BTreeMap<String, String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    values: Option<Value>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    handler: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    select: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    push: Option<Value>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    replace: Option<String>,
37}
38
39impl HxLocation {
40    /// Create a new HX-Location builder pointing to the provided path.
41    pub fn new(path: impl Into<String>) -> Self {
42        HxLocation {
43            path: path.into(),
44            target: None,
45            source: None,
46            event: None,
47            swap: None,
48            headers: BTreeMap::new(),
49            values: None,
50            handler: None,
51            select: None,
52            push: None,
53            replace: None,
54        }
55    }
56
57    /// Override which element receives the swap.
58    pub fn target(mut self, selector: impl Into<String>) -> Self {
59        self.target = Some(selector.into());
60        self
61    }
62
63    /// Set the selector for the element that should be treated as the source.
64    pub fn source(mut self, selector: impl Into<String>) -> Self {
65        self.source = Some(selector.into());
66        self
67    }
68
69    /// Specify an event name to trigger on the client before navigation.
70    pub fn event(mut self, event: impl Into<String>) -> Self {
71        self.event = Some(event.into());
72        self
73    }
74
75    /// Change the swap behaviour for the follow-up request.
76    pub fn swap(mut self, swap: SwapType) -> Self {
77        self.swap = Some(swap.to_string());
78        self
79    }
80
81    /// Provide a custom client-side response handler.
82    pub fn handler(mut self, handler: impl Into<String>) -> Self {
83        self.handler = Some(handler.into());
84        self
85    }
86
87    /// Restrict the response fragment that htmx should swap.
88    pub fn select(mut self, selector: impl Into<String>) -> Self {
89        self.select = Some(selector.into());
90        self
91    }
92
93    /// Add a custom header to the follow-up request.
94    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
95        self.headers.insert(name.into(), value.into());
96        self
97    }
98
99    /// Extend the custom headers with any iterator of key/value pairs.
100    pub fn headers<I, K, V>(mut self, headers: I) -> Self
101    where
102        I: IntoIterator<Item = (K, V)>,
103        K: Into<String>,
104        V: Into<String>,
105    {
106        self.headers
107            .extend(headers.into_iter().map(|(k, v)| (k.into(), v.into())));
108        self
109    }
110
111    /// Serialize any value into the HX-Location `values` object.
112    pub fn values<T>(mut self, values: T) -> serde_json::Result<Self>
113    where
114        T: Serialize,
115    {
116        self.values = Some(serde_json::to_value(values)?);
117        Ok(self)
118    }
119
120    /// Prevent htmx from pushing a new history entry.
121    pub fn disable_push(mut self) -> Self {
122        self.push = Some(Value::Bool(false));
123        self
124    }
125
126    /// Override the history push path for the follow-up request.
127    pub fn push_path(mut self, path: impl Into<String>) -> Self {
128        self.push = Some(Value::String(path.into()));
129        self
130    }
131
132    /// Replace the browser history entry with the provided path.
133    pub fn replace(mut self, path: impl Into<String>) -> Self {
134        self.replace = Some(path.into());
135        self
136    }
137
138    pub(crate) fn into_header_value(self) -> String {
139        serde_json::to_string(&self).expect("HxLocation serialization failed")
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[derive(Serialize)]
148    struct Payload<'a> {
149        id: u32,
150        name: &'a str,
151    }
152
153    #[test]
154    fn values_accepts_serializable_types() {
155        let location = HxLocation::new("/path")
156            .values(Payload {
157                id: 7,
158                name: "demo",
159            })
160            .expect("serialization should succeed");
161
162        let serialized = location.into_header_value();
163        let parsed: Value = serde_json::from_str(&serialized).unwrap();
164
165        assert_eq!(parsed["path"], "/path");
166        assert_eq!(parsed["values"]["id"], 7);
167        assert_eq!(parsed["values"]["name"], "demo");
168    }
169}