diff --git a/assets/js/phoenix_live_view/dom.js b/assets/js/phoenix_live_view/dom.js index 79a58741b1..4543ced1b0 100644 --- a/assets/js/phoenix_live_view/dom.js +++ b/assets/js/phoenix_live_view/dom.js @@ -213,8 +213,12 @@ let DOM = { putTitle(str){ let titleEl = document.querySelector("title") if(titleEl){ - let {prefix, suffix} = titleEl.dataset - document.title = `${prefix || ""}${str}${suffix || ""}` + let {prefix, suffix, default: defaultTitle} = titleEl.dataset + let isEmpty = typeof(str) !== "string" || str.trim() === "" + if(isEmpty && typeof(defaultTitle) !== "string"){ return } + + let inner = isEmpty ? defaultTitle : str + document.title = `${prefix || ""}${inner || ""}${suffix || ""}` } else { document.title = str } diff --git a/assets/js/phoenix_live_view/view.js b/assets/js/phoenix_live_view/view.js index ff6f814fd6..7a14168b03 100644 --- a/assets/js/phoenix_live_view/view.js +++ b/assets/js/phoenix_live_view/view.js @@ -302,7 +302,7 @@ export default class View { this.log(type, () => ["", clone(rawDiff)]) let {diff, reply, events, title} = Rendered.extract(rawDiff) callback({diff, reply, events}) - if(typeof title === "string"){ window.requestAnimationFrame(() => DOM.putTitle(title)) } + if(typeof title === "string" || type == "mount"){ window.requestAnimationFrame(() => DOM.putTitle(title)) } } onJoin(resp){ diff --git a/assets/test/dom_test.js b/assets/test/dom_test.js index 7e2efc321d..732ef85c1f 100644 --- a/assets/test/dom_test.js +++ b/assets/test/dom_test.js @@ -118,6 +118,18 @@ describe("DOM", () => { DOM.putTitle("My Title") expect(document.title).toBe("PRE My Title POST") }) + + test("with default", () => { + appendTitle({default: "DEFAULT", prefix: "PRE ", suffix: " POST"}) + DOM.putTitle(null) + expect(document.title).toBe("PRE DEFAULT POST") + + DOM.putTitle(undefined) + expect(document.title).toBe("PRE DEFAULT POST") + + DOM.putTitle("") + expect(document.title).toBe("PRE DEFAULT POST") + }) }) describe("findExistingParentCIDs", () => { diff --git a/assets/test/test_helpers.js b/assets/test/test_helpers.js index 885ea3acdd..01b155605c 100644 --- a/assets/test/test_helpers.js +++ b/assets/test/test_helpers.js @@ -1,11 +1,18 @@ import View from "phoenix_live_view/view" import {version as liveview_version} from "../package.json" -export let appendTitle = opts => { +export let appendTitle = (opts, innerHTML) => { + Array.from(document.head.querySelectorAll("title")).forEach(el => el.remove()) let title = document.createElement("title") - let {prefix, suffix} = opts + let {prefix, suffix, default: defaultTitle} = opts if(prefix){ title.setAttribute("data-prefix", prefix) } if(suffix){ title.setAttribute("data-suffix", suffix) } + if(defaultTitle){ + title.setAttribute("data-default", defaultTitle) + } else { + title.removeAttribute("data-default") + } + if(innerHTML){ title.innerHTML = innerHTML } document.head.appendChild(title) } @@ -78,5 +85,3 @@ export function liveViewDOM(content){ document.body.appendChild(div) return div } - - diff --git a/assets/test/view_test.js b/assets/test/view_test.js index 73ff1c66db..dd1ee11445 100644 --- a/assets/test/view_test.js +++ b/assets/test/view_test.js @@ -12,7 +12,7 @@ import { PHX_HAS_FOCUSED } from "phoenix_live_view/constants" -import {tag, simulateJoinedView, stubChannel, rootContainer, liveViewDOM, simulateVisibility} from "./test_helpers" +import {tag, simulateJoinedView, stubChannel, rootContainer, liveViewDOM, simulateVisibility, appendTitle} from "./test_helpers" let simulateUsedInput = (input) => { DOM.putPrivate(input, PHX_HAS_FOCUSED, true) @@ -44,9 +44,10 @@ describe("View + DOM", function(){ expect(view.rendered.get()).toEqual(updateDiff) }) - test("applyDiff can set title to falsy values", async () => { - document.title = "Foo" + test("applyDiff with empty title uses default if present", async () => { + appendTitle({}, "Foo") + let titleEl = document.querySelector("title") let liveSocket = new LiveSocket("/live", Socket) let el = liveViewDOM() let updateDiff = { @@ -56,13 +57,17 @@ describe("View + DOM", function(){ } let view = simulateJoinedView(el, liveSocket) - view.applyDiff("update", updateDiff, ({diff, events}) => view.update(diff, events)) + view.applyDiff("mount", updateDiff, ({diff, events}) => view.update(diff, events)) expect(view.el.firstChild.tagName).toBe("H2") expect(view.rendered.get()).toEqual(updateDiff) await new Promise(requestAnimationFrame) - expect(document.title).toBe("") + expect(document.title).toBe("Foo") + titleEl.setAttribute("data-default", "DEFAULT") + view.applyDiff("mount", updateDiff, ({diff, events}) => view.update(diff, events)) + await new Promise(requestAnimationFrame) + expect(document.title).toBe("DEFAULT") }) test("pushWithReply", function(){ diff --git a/lib/phoenix_component.ex b/lib/phoenix_component.ex index 749e5b0ade..219184a09b 100644 --- a/lib/phoenix_component.ex +++ b/lib/phoenix_component.ex @@ -2084,14 +2084,14 @@ defmodule Phoenix.Component do ## Examples ```heex - <.live_title prefix="MyApp – "> - <%= assigns[:page_title] || "Welcome" %> + <.live_title default="Welcome" prefix="MyApp – "> + <%= assigns[:page_title] %> ``` ```heex - <.live_title suffix="- MyApp"> - <%= assigns[:page_title] || "Welcome" %> + <.live_title default="Welcome" suffix="- MyApp"> + <%= assigns[:page_title] %> ``` """ @@ -2101,15 +2101,33 @@ defmodule Phoenix.Component do doc: "A prefix added before the content of `inner_block`." ) + attr.(:default, :string, + default: nil, + doc: "The default title to use if the inner block is empty on regular or connected mounts." + ) + attr.(:suffix, :string, default: nil, doc: "A suffix added after the content of `inner_block`.") slot.(:inner_block, required: true, doc: "Content rendered inside the `title` tag.") def live_title(assigns) do ~H""" - <%= @prefix %><%= render_slot(@inner_block) %><%= @suffix %> + <%= @prefix %><%= render_present(render_slot(@inner_block), @default) %><%= @suffix %> """ end + defp render_present(rendered_block, default) do + block_str = + rendered_block + |> Phoenix.HTML.html_escape() + |> Phoenix.HTML.safe_to_string() + + if String.trim(block_str) == "" do + default + else + rendered_block + end + end + @doc ~S''' Renders a form. diff --git a/test/phoenix_component/components_test.exs b/test/phoenix_component/components_test.exs index bc1d5e8228..c076f12178 100644 --- a/test/phoenix_component/components_test.exs +++ b/test/phoenix_component/components_test.exs @@ -185,6 +185,25 @@ defmodule Phoenix.LiveView.ComponentsTest do assert t2h(~H|<.live_title>My Title|) == ~X|My Title| end + + test "default with blank inner block" do + assigns = %{ + val: """ + + + """ + } + + assert t2h(~H|<.live_title default="DEFAULT" phx-no-format> <%= @val %> |) == + ~X|DEFAULT| + end + + test "default with present inner block" do + assigns = %{val: "My Title"} + + assert t2h(~H|<.live_title default="DEFAULT" phx-no-format> <%= @val %> |) == + ~X| My Title | + end end describe "dynamic_tag/1" do