Skip to content

Commit

Permalink
Merge pull request #457 from phoenixframework/sd-before_closing_head_tag
Browse files Browse the repository at this point in the history
Allow users to render content in the <head>
  • Loading branch information
SteffenDE authored Nov 11, 2024
2 parents d1578d8 + e7728dd commit 90ca015
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 11,913 deletions.
2 changes: 1 addition & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let Hooks = {
let socketPath = document.querySelector("html").getAttribute("phx-socket") || "/live"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveView.LiveSocket(socketPath, Phoenix.Socket, {
hooks: Hooks,
hooks: { ...Hooks, ...window.LiveDashboard.customHooks },
params: (liveViewName) => {
return {
_csrf_token: csrfToken,
Expand Down
11,909 changes: 3 additions & 11,906 deletions dist/css/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/js/app.js.map

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions lib/phoenix/live_dashboard/layout_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,20 @@ defmodule Phoenix.LiveDashboard.LayoutView do
)
end
end

defp custom_head_tags(assigns, key) do
case assigns do
%{^key => components} when is_list(components) ->
assigns = assign(assigns, :components, components)

~H"""
<%= for component <- @components do %>
<%= component.(assigns) %>
<% end %>
"""

_ ->
nil
end
end
end
10 changes: 10 additions & 0 deletions lib/phoenix/live_dashboard/layouts/dash.html.heex
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
<!DOCTYPE html>
<html lang="en" phx-socket={live_socket_path(@conn)}>
<head>
<script nonce={csp_nonce(@conn, :script)}>
window.LiveDashboard = {
customHooks: {},
registerCustomHooks(hooks) {
this.customHooks = {...this.customHooks, ...hooks}
}
}
</script>
<%= custom_head_tags(assigns, :after_opening_head_tag) %>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no, user-scalable=no"/>
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<title><%= assigns[:page_title] || "Phoenix LiveDashboard" %></title>
<link rel="stylesheet" nonce={csp_nonce(@conn, :style)} href={asset_path(@conn, :css)}>
<script nonce={csp_nonce(@conn, :script)} src={asset_path(@conn, :js)} defer></script>
<%= custom_head_tags(assigns, :before_closing_head_tag) %>
</head>
<body>
<div class="d-flex flex-column align-items-stretch layout-wrapper">
Expand Down
104 changes: 103 additions & 1 deletion lib/phoenix/live_dashboard/page_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
We currently support `card/1`, `fields_card/1`, `row/1`,
`shared_usage_card/1`, and `usage_card/1`;
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and `live_table/1`.
## Helpers
Expand All @@ -105,6 +105,84 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
helpers are: `live_dashboard_path/2`, `live_dashboard_path/3`,
`encode_app/1`, `encode_ets/1`, `encode_pid/1`, `encode_port/1`,
and `encode_socket/1`.
## Custom Hooks
If your page needs to register custom hooks, you can use the `register_after_opening_head_tag/2`
function. Because the hooks need to be available on the dead render in the layout, before the
LiveView's LiveSocket is configured, your need to do this inside an `on_mount` hook:
```elixir
defmodule MyAppWeb.MyLiveDashboardHooks do
import Phoenix.LiveView
import Phoenix.Component
alias Phoenix.LiveDashboard.PageBuilder
def on_mount(:default, _params, _session, socket) do
{:cont, PageBuilder.register_after_opening_head_tag(socket, &after_opening_head_tag/1)}
end
defp after_opening_head_tag(assigns) do
~H\"\"\"
<script nonce={@csp_nonces[:script]}>
window.LiveDashboard.registerCustomHooks({
MyHook: {
mounted() {
// do something
}
}
})
</script>
\"\"\"
end
end
defmodule MyAppWeb.MyCustomPage do
...
end
```
And then add it to the list of `on_mount` hooks in the `live_dashboard` router configuration:
```elixir
live_dashboard "/dashboard",
additional_pages: [
route_name: MyAppWeb.MyCustomPage
],
on_mount: [
MyAppWeb.MyLiveDashboardHooks
]
```
The LiveDashboard provides a function `window.LiveDashboard.registerCustomHooks({ ... })` that you can call
with an object of hook declarations.
Note that in order to use external libraries, you will either need to include them from
a CDN, or bundle them yourself and include them from your app's static paths.
> #### A note on CSPs and libraries {: .info}
>
> Phoenix LiveDashboard supports CSP nonces for its own assets, configurable using the
> `Phoenix.LiveDashboard.Router.live_dashboard/2` macro by setting the `:csp_nonce_assign_key`
> option. If you are building a library, ensure that you render those CSP nonces on any scripts,
> styles or images of your page. The nonces are passed to your custom page under the `:csp_nonces` assign
> and also available in the `after_opening_head_tag` component.
>
> You should use those when including scripts or styles like this:
>
> ```heex
> <script nonce={@csp_nonces[:script]}>...</script>
> <script nonce={@csp_nonces[:script]} src="..."></script>
> <style nonce={@csp_nonces[:style]}>...</style>
> <link rel="stylesheet" href="..." nonce={@csp_nonces[:style]}>
> ```
>
> This ensures that your custom page can be used when a CSP is in place using the mechanism
> supported by Phoenix LiveDashboard.
>
> If your custom page needs a different CSP policy, for example due to inline styles set by scripts,
> please consider documenting these requirements.
"""

use Phoenix.Component
Expand Down Expand Up @@ -971,6 +1049,30 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
live_dashboard_path(socket, route, node, old_params, new_params)
end

@doc """
Registers a component to be rendered after the opening head tag in the layout.
"""
def register_after_opening_head_tag(socket, component) do
register_head(socket, component, :after_opening_head_tag)
end

@doc """
Registers a component to be rendered before the closing head tag in the layout.
"""
def register_before_closing_head_tag(socket, component) do
register_head(socket, component, :before_closing_head_tag)
end

defp register_head(socket, component, assign) do
case socket do
%{assigns: %{^assign => [_ | _]}} ->
update(socket, assign, fn existing -> [component | existing] end)

_ ->
assign(socket, assign, [component])
end
end

# TODO: Remove this and the conditional on Phoenix v1.7+
@compile {:no_warn_undefined, Phoenix.VerifiedRoutes}

Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ defmodule Phoenix.LiveDashboard.MixProject do
{:stream_data, "~> 0.1", only: :test},
{:ecto_sqlite3, "~> 0.9.1", only: [:dev, :test]},
{:ex_doc, "~> 0.21", only: :docs},
{:makeup_eex, ">= 0.1.1", only: :docs},
{:esbuild, "~> 0.5", only: :dev},
{:dart_sass, "~> 0.7", only: :dev}
]
Expand Down
6 changes: 4 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_eex": {:hex, :makeup_eex, "1.0.0", "436d4c00204c250b17a775d64e197798aaf374627e6a4f2d3fd3074a8db61db4", [:mix], [{:makeup, "~> 1.2.1 or ~> 1.3", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "3bb699bc519e4f509f1bf8a2e0ba0e08429edf3580053cd31a4f9c1bc5da86c8"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"makeup_html": {:hex, :makeup_html, "0.1.2", "19d4050c0978a4f1618ffe43054c0049f91fe5feeb9ae8d845b5dc79c6008ae5", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b7fb9afedd617d167e6644a0430e49c1279764bfd3153da716d4d2459b0998c5"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"myxql": {:hex, :myxql, "0.6.3", "3d77683a09f1227abb8b73d66b275262235c5cae68182f0cfa5897d72a03700e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "af9eb517ddaced5c5c28e8749015493757fd4413f2cfccea449c466d405d9f51"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
Expand Down

0 comments on commit 90ca015

Please sign in to comment.