Current section
Files
Jump to
Current section
Files
lib/hypa/plug.ex
defmodule Hypa.Plug do
@moduledoc """
A `Plug` offering functions useful for applications using HTMX.
This plug invokes the following function plugs:
- `detect_htmx_request/2` - Adds a `:htmx` map to `conn.assigns` containing htmx request headers.
- `add_htmx_cache_control/2` - Adds `hx-request` to the response's `vary` header
The following functions are also invoked if `Phoenix.Controller` is present at compile-time:
- `put_htmx_layout/2` - Disables the root and app layouts if responding to an htmx request
"""
@moduledoc since: "0.1.0"
use Plug.Builder
plug :detect_htmx_request
plug :add_htmx_cache_control
if Code.ensure_loaded?(Phoenix.Controller) do
plug :put_htmx_layout
end
@doc """
Extracts HTMX headers into the conn's assigns.
See [Request Headers Reference] for documentation on htmx's header values.
[Request Headers Reference]: https://htmx.org/reference/#request_headers
## Examples
iex> conn(:get, "/hello")
...> |> merge_req_headers([
...> {"hx-request", "true"},
...> {"hx-current-url", "/current"},
...> {"hx-history-restore-request", "false"},
...> {"hx-target", "my-target-element-id"},
...> {"hx-trigger-name", "my-triggered-element-name"},
...> {"hx-trigger", "my-triggered-element-id"}
...> ])
...> |> Hypa.Plug.detect_htmx_request(Hypa.Plug.init([]))
...> |> Map.get(:assigns)
...> |> Map.get(:htmx)
%{
request: true,
boosted: false,
current_url: "/current",
history_restore_request: false,
prompt: nil,
target: "my-target-element-id",
trigger: "my-triggered-element-id",
trigger_name: "my-triggered-element-name"
}
If the request does not have htmx headers, the `:htmx` assign will not be present.
iex> conn(:get, "/htmxless")
...> |> Hypa.Plug.detect_htmx_request(Hypa.Plug.init([]))
...> |> Map.get(:assigns)
...> |> Map.has_key?(:htmx)
false
"""
@doc since: "0.1.0"
@spec detect_htmx_request(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
def detect_htmx_request(%Plug.Conn{} = conn, _opts) do
if get_req_header(conn, "hx-request") == ["true"] do
conn =
assign(conn, :htmx, %{
request: true,
boosted: get_req_header(conn, "hx-boosted") != [],
current_url: List.first(get_req_header(conn, "hx-current-url")),
history_restore_request: get_req_header(conn, "hx-history-restore-request") == ["true"],
prompt: List.first(get_req_header(conn, "hx-prompt")),
target: List.first(get_req_header(conn, "hx-target")),
trigger_name: List.first(get_req_header(conn, "hx-trigger-name")),
trigger: List.first(get_req_header(conn, "hx-trigger"))
})
conn
else
conn
end
end
@doc """
Adds htmx to the response's cache headers in order for caching to work correctly.
## Examples
iex> conn(:get, "/hello")
...> |> Hypa.Plug.add_htmx_cache_control(Hypa.Plug.init([]))
...> |> get_resp_header("vary")
["hx-request"]
"""
@doc since: "0.2.0"
@spec add_htmx_cache_control(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
def add_htmx_cache_control(%Plug.Conn{} = conn, _opts) do
prepend_resp_headers(conn, [{"vary", "hx-request"}])
end
if Code.ensure_loaded?(Phoenix.Controller) do
@doc """
Disable Phoenix layouts in responses to htmx requests.
Only available when Phoenix is available.
## Examples
The root layout (usually containing `<head>` content) is disabled for all htmx requests.
iex> conn(:get, "/hello")
...> |> assign(:htmx, %{request: true})
...> |> Hypa.Plug.put_htmx_layout(Hypa.Plug.init([]))
...> |> Phoenix.Controller.root_layout("html")
false
Usually, the app layout is disabled for htmx requests as well.
iex> conn(:get, "/hello")
...> |> assign(:htmx, %{request: true})
...> |> Hypa.Plug.put_htmx_layout(Hypa.Plug.init([]))
...> |> Phoenix.Controller.layout("html")
false
However, requests made using `hx-boost` are given the app layout.
The root layout is still disabled for boosted requests.
iex> conn(:get, "/hello")
...> |> Phoenix.Controller.put_layout(html: {MyApp.Layouts, :app})
...> |> assign(:htmx, %{request: true, boosted: true})
...> |> Hypa.Plug.put_htmx_layout(Hypa.Plug.init([]))
...> |> Phoenix.Controller.layout("html")
{MyApp.Layouts, :app}
This is also the case for history restore requests.
The root layout is disabled for history restore requests as well.
iex> conn(:get, "/hello")
...> |> Phoenix.Controller.put_layout(html: {MyApp.Layouts, :app})
...> |> assign(:htmx, %{request: true, history_restore_request: true})
...> |> Hypa.Plug.put_htmx_layout(Hypa.Plug.init([]))
...> |> Phoenix.Controller.layout("html")
{MyApp.Layouts, :app}
"""
@doc since: "0.2.0"
@spec put_htmx_layout(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
def put_htmx_layout(%Plug.Conn{} = conn, _opts) do
if get_in(conn.assigns, [:htmx, :request]) do
conn = Phoenix.Controller.put_root_layout(conn, html: false)
if conn.assigns.htmx[:boosted] || conn.assigns.htmx[:history_restore_request] do
conn
else
Phoenix.Controller.put_layout(conn, html: false)
end
else
conn
end
end
end
end