Skip to content

Commit c44ed4d

Browse files
committed
Allow users to render content in the <head>
Relates to: #455
1 parent d1578d8 commit c44ed4d

File tree

7 files changed

+123
-11914
lines changed

7 files changed

+123
-11914
lines changed

assets/js/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ let Hooks = {
1818
PhxRememberRefresh: PhxRememberRefresh
1919
}
2020

21+
window.customHooks = window.customHooks || {}
22+
2123
let socketPath = document.querySelector("html").getAttribute("phx-socket") || "/live"
2224
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
2325
let liveSocket = new LiveView.LiveSocket(socketPath, Phoenix.Socket, {
24-
hooks: Hooks,
26+
hooks: { ...Hooks, ...window.customHooks },
2527
params: (liveViewName) => {
2628
return {
2729
_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: 4 additions & 4 deletions
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<!DOCTYPE html>
22
<html lang="en" phx-socket={live_socket_path(@conn)}>
33
<head>
4+
<%= custom_head_tags(assigns, :after_opening_head_tag) %>
45
<meta charset="utf-8"/>
56
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
67
<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"/>
78
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
89
<title><%= assigns[:page_title] || "Phoenix LiveDashboard" %></title>
910
<link rel="stylesheet" nonce={csp_nonce(@conn, :style)} href={asset_path(@conn, :css)}>
1011
<script nonce={csp_nonce(@conn, :script)} src={asset_path(@conn, :js)} defer></script>
12+
<%= custom_head_tags(assigns, :before_closing_head_tag) %>
1113
</head>
1214
<body>
1315
<div class="d-flex flex-column align-items-stretch layout-wrapper">

lib/phoenix/live_dashboard/page_builder.ex

Lines changed: 93 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,74 @@ 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>
129+
window.customHooks = {
130+
...(window.customHooks || {}),
131+
MyHook: {
132+
mounted() {
133+
// do something
134+
}
135+
}
136+
}
137+
</script>
138+
\"\"\"
139+
end
140+
end
141+
142+
defmodule MyAppWeb.MyCustomPage do
143+
...
144+
end
145+
```
146+
147+
And then add it to the list of `on_mount` hooks in the `live_dashboard` router configuration:
148+
149+
```elixir
150+
live_dashboard "/dashboard",
151+
additional_pages: [
152+
route_name: MyAppWeb.MyCustomPage
153+
],
154+
on_mount: [
155+
MyAppWeb.MyLiveDashboardHooks
156+
]
157+
```
158+
159+
The LiveDashboard will merge the `window.customHooks` object into the hooks that are
160+
configured on the LiveSocket.
161+
162+
> #### Warning {: .warning}
163+
>
164+
> If you are building a library that will be used by others, ensure that you are
165+
> not overwriting the `window.customHooks` object instead of extending it.
166+
>
167+
> Instead of `window.customHooks = {...}`,
168+
> use `window.customHooks = {...(window.customHooks || {}), ...}`.
169+
170+
Note that in order to use external libraries, you will either need to include them from
171+
a CDN, or bundle them yourself and include them from your apps static paths.
172+
173+
Also, you are responsible for ensuring that your Content Security Policy (CSP) allows
174+
the hooks to be executed. If you are building a library that will be used by others,
175+
consider including a valid nonce on your script tags.
108176
"""
109177

110178
use Phoenix.Component
@@ -971,6 +1039,30 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
9711039
live_dashboard_path(socket, route, node, old_params, new_params)
9721040
end
9731041

1042+
@doc """
1043+
Registers a component to be rendered after the opening head tag in the layout.
1044+
"""
1045+
def register_after_opening_head_tag(socket, component) do
1046+
register_head(socket, component, :after_opening_head_tag)
1047+
end
1048+
1049+
@doc """
1050+
Registers a component to be rendered before the closing head tag in the layout.
1051+
"""
1052+
def register_before_closing_head_tag(socket, component) do
1053+
register_head(socket, component, :before_closing_head_tag)
1054+
end
1055+
1056+
defp register_head(socket, component, assign) do
1057+
case socket do
1058+
%{assigns: %{^assign => [_ | _]}} ->
1059+
update(socket, assign, fn existing -> [component | existing] end)
1060+
1061+
_ ->
1062+
assign(socket, assign, [component])
1063+
end
1064+
end
1065+
9741066
# TODO: Remove this and the conditional on Phoenix v1.7+
9751067
@compile {:no_warn_undefined, Phoenix.VerifiedRoutes}
9761068

0 commit comments

Comments
 (0)