Skip to content

Commit e2a4e07

Browse files
feat: support the full HTMX trigger header API
get_triggers may now return a dict keyed by trigger phase ("trigger" -> HX-Trigger, "after_settle" -> HX-Trigger-After-Settle, "after_swap" -> HX-Trigger-After-Swap) in addition to the existing list (which still maps to HX-Trigger). Header building moves to a new get_trigger_headers(); format_triggers() is kept for backwards compatibility. FormModalHxRequest.get_triggers handles both shapes so closeHxModal survives a dict return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b54a7ff commit e2a4e07

1 file changed

Lines changed: 71 additions & 14 deletions

File tree

hx_requests/hx_requests.py

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ class BaseHxRequest:
9999
refresh_views_context_on_POST: bool = False
100100
use_current_url: bool = False
101101

102+
#: Maps the friendly phase keys accepted in a dict return from
103+
#: :meth:`get_triggers` to their HTMX response-header names.
104+
trigger_header_map: dict[str, str] = {
105+
"trigger": "HX-Trigger",
106+
"after_settle": "HX-Trigger-After-Settle",
107+
"after_swap": "HX-Trigger-After-Swap",
108+
}
109+
102110
@cached_property
103111
def is_post_request(self):
104112
"""
@@ -193,33 +201,78 @@ def get_headers(self, **kwargs) -> Dict:
193201
if self.no_swap:
194202
headers["HX-Reswap"] = "none"
195203

196-
triggers = self.format_triggers(**kwargs)
197-
if triggers:
198-
headers["HX-Trigger"] = triggers
204+
headers.update(self.get_trigger_headers(**kwargs))
199205
return headers
200206

201-
def get_triggers(self, **kwargs) -> List[Union[str, dict]]:
207+
def get_triggers(self, **kwargs) -> Union[List[Union[str, dict]], Dict[str, list]]:
202208
"""
203209
Override to set the triggers for the response.
204210
205-
Return a list of triggers. Each trigger can be:
206-
- A string for a simple trigger (e.g. ``"myEvent"``).
207-
- A dict to pass details with the trigger
208-
(e.g. ``{"showMessage": {"level": "info", "message": "Saved!"}}``).
211+
Return either:
212+
213+
- A **list** of triggers, emitted on the ``HX-Trigger`` header (fired
214+
as soon as the response is received). Each item can be:
215+
216+
- A string for a simple trigger (e.g. ``"myEvent"``).
217+
- A dict to pass details with the trigger
218+
(e.g. ``{"showMessage": {"level": "info", "message": "Saved!"}}``).
219+
220+
- A **dict** keyed by trigger phase to target the full set of HTMX
221+
trigger headers, where each value is a list of the same shape::
222+
223+
{
224+
"trigger": ["eventA"], # HX-Trigger
225+
"after_settle": ["eventB"], # HX-Trigger-After-Settle
226+
"after_swap": [{"eventC": {...}}], # HX-Trigger-After-Swap
227+
}
209228
210-
When any dict is present, the header is formatted as a JSON object
211-
per the `HX-Trigger response header spec <https://htmx.org/headers/hx-trigger/>`_.
229+
Within any list, when a dict trigger is present the header value is
230+
formatted as a JSON object per the `HX-Trigger response header spec
231+
<https://htmx.org/headers/hx-trigger/>`_.
212232
"""
213233
return []
214234

235+
def get_trigger_headers(self, **kwargs) -> Dict[str, str]:
236+
"""
237+
Build the HTMX trigger response headers from :meth:`get_triggers`.
238+
239+
Returns a ``{header_name: header_value}`` dict covering ``HX-Trigger``,
240+
``HX-Trigger-After-Settle`` and ``HX-Trigger-After-Swap`` as needed.
241+
Phases with no triggers are omitted.
242+
"""
243+
triggers = self.get_triggers(**kwargs)
244+
# A plain list keeps the historical behavior: everything on HX-Trigger.
245+
if not isinstance(triggers, dict):
246+
triggers = {"trigger": triggers}
247+
248+
headers = {}
249+
for key, header_name in self.trigger_header_map.items():
250+
value = self._format_trigger_value(triggers.get(key) or [])
251+
if value:
252+
headers[header_name] = value
253+
return headers
254+
215255
def format_triggers(self, **kwargs) -> str:
216256
"""
217-
Format triggers for the ``HX-Trigger`` header.
257+
Format the ``HX-Trigger`` header value from :meth:`get_triggers`.
218258
219-
If all triggers are plain strings, they are comma-separated.
220-
If any trigger carries details (dict), the header is JSON-encoded.
259+
Retained for backwards compatibility; only covers the ``HX-Trigger``
260+
header. For the full set of trigger headers see
261+
:meth:`get_trigger_headers`.
221262
"""
222263
triggers = self.get_triggers(**kwargs)
264+
if isinstance(triggers, dict):
265+
triggers = triggers.get("trigger") or []
266+
return self._format_trigger_value(triggers)
267+
268+
@staticmethod
269+
def _format_trigger_value(triggers: List[Union[str, dict]]) -> str:
270+
"""
271+
Format a single list of triggers into one header value.
272+
273+
If all triggers are plain strings, they are comma-separated. If any
274+
trigger carries details (dict), the value is JSON-encoded.
275+
"""
223276
has_details = any(isinstance(t, dict) for t in triggers)
224277
if not has_details:
225278
return ", ".join(triggers)
@@ -621,7 +674,11 @@ def get_triggers(self, **kwargs) -> list:
621674
"""
622675
triggers = super().get_triggers(**kwargs)
623676
if self.is_post_request and self.form.is_valid() and self.close_modal_on_save:
624-
triggers.append("closeHxModal")
677+
# Support both the list return and the phase-keyed dict return.
678+
if isinstance(triggers, dict):
679+
triggers.setdefault("trigger", []).append("closeHxModal")
680+
else:
681+
triggers.append("closeHxModal")
625682
return triggers
626683

627684
def get_headers(self, **kwargs) -> Dict:

0 commit comments

Comments
 (0)