Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to render content in the <head> #457

Merged
merged 2 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ let Hooks = {
PhxRememberRefresh: PhxRememberRefresh
}

window.customHooks = window.customHooks || {}

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.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.

8 changes: 4 additions & 4 deletions 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
2 changes: 2 additions & 0 deletions lib/phoenix/live_dashboard/layouts/dash.html.heex
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<!DOCTYPE html>
<html lang="en" phx-socket={live_socket_path(@conn)}>
<head>
<%= 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
94 changes: 93 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,74 @@ 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)}
SteffenDE marked this conversation as resolved.
Show resolved Hide resolved
end

defp after_opening_head_tag(assigns) do
~H\"\"\"
<script>
SteffenDE marked this conversation as resolved.
Show resolved Hide resolved
window.customHooks = {
...(window.customHooks || {}),
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 will merge the `window.customHooks` object into the hooks that are
configured on the LiveSocket.

> #### Warning {: .warning}
>
> If you are building a library that will be used by others, ensure that you are
> not overwriting the `window.customHooks` object instead of extending it.
>
> Instead of `window.customHooks = {...}`,
> use `window.customHooks = {...(window.customHooks || {}), ...}`.

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 apps static paths.

Also, you are responsible for ensuring that your Content Security Policy (CSP) allows
the hooks to be executed. If you are building a library that will be used by others,
consider including a valid nonce on your script tags.
"""

use Phoenix.Component
Expand Down Expand Up @@ -971,6 +1039,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
Loading