Skip to content

Commit 90ca015

Browse files
authored
Merge pull request #457 from phoenixframework/sd-before_closing_head_tag
Allow users to render content in the <head>
2 parents d1578d8 + e7728dd commit 90ca015

File tree

9 files changed

+141
-11913
lines changed

9 files changed

+141
-11913
lines changed

assets/js/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let Hooks = {
2121
let socketPath = document.querySelector("html").getAttribute("phx-socket") || "/live"
2222
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
2323
let liveSocket = new LiveView.LiveSocket(socketPath, Phoenix.Socket, {
24-
hooks: Hooks,
24+
hooks: { ...Hooks, ...window.LiveDashboard.customHooks },
2525
params: (liveViewName) => {
2626
return {
2727
_csrf_token: csrfToken,

dist/css/app.css

Lines changed: 3 additions & 11906 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/js/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/js/app.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/phoenix/live_dashboard/layout_view.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,20 @@ defmodule Phoenix.LiveDashboard.LayoutView do
3737
)
3838
end
3939
end
40+
41+
defp custom_head_tags(assigns, key) do
42+
case assigns do
43+
%{^key => components} when is_list(components) ->
44+
assigns = assign(assigns, :components, components)
45+
46+
~H"""
47+
<%= for component <- @components do %>
48+
<%= component.(assigns) %>
49+
<% end %>
50+
"""
51+
52+
_ ->
53+
nil
54+
end
55+
end
4056
end

lib/phoenix/live_dashboard/layouts/dash.html.heex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
<!DOCTYPE html>
22
<html lang="en" phx-socket={live_socket_path(@conn)}>
33
<head>
4+
<script nonce={csp_nonce(@conn, :script)}>
5+
window.LiveDashboard = {
6+
customHooks: {},
7+
registerCustomHooks(hooks) {
8+
this.customHooks = {...this.customHooks, ...hooks}
9+
}
10+
}
11+
</script>
12+
<%= custom_head_tags(assigns, :after_opening_head_tag) %>
413
<meta charset="utf-8"/>
514
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
615
<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"/>
716
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
817
<title><%= assigns[:page_title] || "Phoenix LiveDashboard" %></title>
918
<link rel="stylesheet" nonce={csp_nonce(@conn, :style)} href={asset_path(@conn, :css)}>
1019
<script nonce={csp_nonce(@conn, :script)} src={asset_path(@conn, :js)} defer></script>
20+
<%= custom_head_tags(assigns, :before_closing_head_tag) %>
1121
</head>
1222
<body>
1323
<div class="d-flex flex-column align-items-stretch layout-wrapper">

lib/phoenix/live_dashboard/page_builder.ex

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
9696
9797
We currently support `card/1`, `fields_card/1`, `row/1`,
9898
`shared_usage_card/1`, and `usage_card/1`;
99-
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
99+
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
100100
and `live_table/1`.
101101
102102
## Helpers
@@ -105,6 +105,84 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
105105
helpers are: `live_dashboard_path/2`, `live_dashboard_path/3`,
106106
`encode_app/1`, `encode_ets/1`, `encode_pid/1`, `encode_port/1`,
107107
and `encode_socket/1`.
108+
109+
## Custom Hooks
110+
111+
If your page needs to register custom hooks, you can use the `register_after_opening_head_tag/2`
112+
function. Because the hooks need to be available on the dead render in the layout, before the
113+
LiveView's LiveSocket is configured, your need to do this inside an `on_mount` hook:
114+
115+
```elixir
116+
defmodule MyAppWeb.MyLiveDashboardHooks do
117+
import Phoenix.LiveView
118+
import Phoenix.Component
119+
120+
alias Phoenix.LiveDashboard.PageBuilder
121+
122+
def on_mount(:default, _params, _session, socket) do
123+
{:cont, PageBuilder.register_after_opening_head_tag(socket, &after_opening_head_tag/1)}
124+
end
125+
126+
defp after_opening_head_tag(assigns) do
127+
~H\"\"\"
128+
<script nonce={@csp_nonces[:script]}>
129+
window.LiveDashboard.registerCustomHooks({
130+
MyHook: {
131+
mounted() {
132+
// do something
133+
}
134+
}
135+
})
136+
</script>
137+
\"\"\"
138+
end
139+
end
140+
141+
defmodule MyAppWeb.MyCustomPage do
142+
...
143+
end
144+
```
145+
146+
And then add it to the list of `on_mount` hooks in the `live_dashboard` router configuration:
147+
148+
```elixir
149+
live_dashboard "/dashboard",
150+
additional_pages: [
151+
route_name: MyAppWeb.MyCustomPage
152+
],
153+
on_mount: [
154+
MyAppWeb.MyLiveDashboardHooks
155+
]
156+
```
157+
158+
The LiveDashboard provides a function `window.LiveDashboard.registerCustomHooks({ ... })` that you can call
159+
with an object of hook declarations.
160+
161+
Note that in order to use external libraries, you will either need to include them from
162+
a CDN, or bundle them yourself and include them from your app's static paths.
163+
164+
> #### A note on CSPs and libraries {: .info}
165+
>
166+
> Phoenix LiveDashboard supports CSP nonces for its own assets, configurable using the
167+
> `Phoenix.LiveDashboard.Router.live_dashboard/2` macro by setting the `:csp_nonce_assign_key`
168+
> option. If you are building a library, ensure that you render those CSP nonces on any scripts,
169+
> styles or images of your page. The nonces are passed to your custom page under the `:csp_nonces` assign
170+
> and also available in the `after_opening_head_tag` component.
171+
>
172+
> You should use those when including scripts or styles like this:
173+
>
174+
> ```heex
175+
> <script nonce={@csp_nonces[:script]}>...</script>
176+
> <script nonce={@csp_nonces[:script]} src="..."></script>
177+
> <style nonce={@csp_nonces[:style]}>...</style>
178+
> <link rel="stylesheet" href="..." nonce={@csp_nonces[:style]}>
179+
> ```
180+
>
181+
> This ensures that your custom page can be used when a CSP is in place using the mechanism
182+
> supported by Phoenix LiveDashboard.
183+
>
184+
> If your custom page needs a different CSP policy, for example due to inline styles set by scripts,
185+
> please consider documenting these requirements.
108186
"""
109187

110188
use Phoenix.Component
@@ -971,6 +1049,30 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
9711049
live_dashboard_path(socket, route, node, old_params, new_params)
9721050
end
9731051

1052+
@doc """
1053+
Registers a component to be rendered after the opening head tag in the layout.
1054+
"""
1055+
def register_after_opening_head_tag(socket, component) do
1056+
register_head(socket, component, :after_opening_head_tag)
1057+
end
1058+
1059+
@doc """
1060+
Registers a component to be rendered before the closing head tag in the layout.
1061+
"""
1062+
def register_before_closing_head_tag(socket, component) do
1063+
register_head(socket, component, :before_closing_head_tag)
1064+
end
1065+
1066+
defp register_head(socket, component, assign) do
1067+
case socket do
1068+
%{assigns: %{^assign => [_ | _]}} ->
1069+
update(socket, assign, fn existing -> [component | existing] end)
1070+
1071+
_ ->
1072+
assign(socket, assign, [component])
1073+
end
1074+
end
1075+
9741076
# TODO: Remove this and the conditional on Phoenix v1.7+
9751077
@compile {:no_warn_undefined, Phoenix.VerifiedRoutes}
9761078

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ defmodule Phoenix.LiveDashboard.MixProject do
6565
{:stream_data, "~> 0.1", only: :test},
6666
{:ecto_sqlite3, "~> 0.9.1", only: [:dev, :test]},
6767
{:ex_doc, "~> 0.21", only: :docs},
68+
{:makeup_eex, ">= 0.1.1", only: :docs},
6869
{:esbuild, "~> 0.5", only: :dev},
6970
{:dart_sass, "~> 0.7", only: :dev}
7071
]

mix.lock

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
2323
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
2424
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
25-
"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"},
26-
"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"},
25+
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
26+
"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"},
27+
"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"},
2728
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
29+
"makeup_html": {:hex, :makeup_html, "0.1.2", "19d4050c0978a4f1618ffe43054c0049f91fe5feeb9ae8d845b5dc79c6008ae5", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b7fb9afedd617d167e6644a0430e49c1279764bfd3153da716d4d2459b0998c5"},
2830
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
2931
"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"},
3032
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},

0 commit comments

Comments
 (0)