Repository: phoenixframework/phoenix
Pull request #5095 (2022-11-10 04:04:01)

Fix layouts module in component guide
First time contributor here. 😅  

Working through the guide and ran into errors as it was written. I believe I've updated the guide properly but happy to make changes if not.


Patch:
diff --git a/guides/components.md b/guides/components.md
index d603a0a7c..8abe91924 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -65,19 +65,21 @@ Open up our application layout template, `lib/hello_web/components/layouts/root.
 </.live_title>
 ```
 
-to call a `title/0` function, like this.
+to call a `title/1` function, like this.
 
 ```heex
 <.title suffix=" · Phoenix Framework" />
 ```
 
-Now let's add a `title/0` function to our `Layouts` module:
+Now let's add a `title/1` function to our `Layouts` module:
 
 ```elixir
 defmodule HelloWeb.Layouts do
-  use HelloWeb, :view
+  use HelloWeb, :html
+
+  embed_templates "layouts/*"
 
-  attr :suffix, default: nil
+  attr :suffix, :string, default: nil
 
   def title(assigns) do
     ~H"""
diff --git a/guides/routing.md b/guides/routing.md
index a7d97f626..472b2f5b5 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -191,7 +191,7 @@ What about paths with query strings? You can either add query string key values
 ~p"/users/17?admin=true&active=false"
 "/users/17?admin=true&active=false"
 
-~p"/users/17?#{[admin: true]"
+~p"/users/17?#{[admin: true]}"
 "/users/17?admin=true"
 ```
 
diff --git a/lib/phoenix/channel.ex b/lib/phoenix/channel.ex
index 1e475b28c..8636e734f 100644
--- a/lib/phoenix/channel.ex
+++ b/lib/phoenix/channel.ex
@@ -46,43 +46,44 @@ defmodule Phoenix.Channel do
 
   After a client has successfully joined a channel, incoming events from the
   client are routed through the channel's `handle_in/3` callbacks. Within these
-  callbacks, you can perform any action. Typically you'll either forward a
-  message to all listeners with `broadcast!/3`, or push a message directly down
-  the socket with `push/3`. Incoming callbacks must return the `socket` to
-  maintain ephemeral state.
+  callbacks, you can perform any action. Incoming callbacks must return the
+  `socket` to maintain ephemeral state.
 
-  Here's an example of receiving an incoming `"new_msg"` event from one client,
-  and broadcasting the message to all topic subscribers for this socket.
+  Typically you'll either forward a message to all listeners with
+  `broadcast!/3` or reply directly to a client event for request/response style
+  messaging.
+
+  General message payloads are received as maps:
 
       def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
-        broadcast!(socket, "new_msg", %{uid: uid, body: body})
-        {:noreply, socket}
+        ...
+        {:reply, :ok, socket}
       end
 
-  General message payloads are received as maps, and binary data payloads are
-  passed as a `{:binary, data}` tuple:
+  Binary data payloads are passed as a `{:binary, data}` tuple:
 
       def handle_in("file_chunk", {:binary, chunk}, socket) do
         ...
         {:reply, :ok, socket}
       end
 
-  You can also push a message directly down the socket, in the form of a map,
-  or a tagged `{:binary, data}` tuple:
+  ## Broadcasts
+ 
+  Here's an example of receiving an incoming `"new_msg"` event from one client,
+  and broadcasting the message to all topic subscribers for this socket.
 
-      # client asks for their current rank, push sent directly as a new event.
-      def handle_in("current_rank", _, socket) do
-        push(socket, "current_rank", %{val: Game.get_rank(socket.assigns[:user])})
-        push(socket, "photo", {:binary, File.read!(socket.assigns.photo_path)})
+      def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
+        broadcast!(socket, "new_msg", %{uid: uid, body: body})
         {:noreply, socket}
       end
 
   ## Replies
 
-  In addition to pushing messages out when you receive a `handle_in` event,
-  you can also reply directly to a client event for request/response style
-  messaging. This is useful when a client must know the result of an operation
-  or to simply ack messages.
+  Replies are useful for acknowledging a client's message or responding with
+  the results of an operation. A reply is sent only to the client connected to
+  the current channel process. Behind the scenes, they include the client
+  message `ref`, which allows the client to correlate the reply it receives
+  with the message it sent.
 
   For example, imagine creating a resource and replying with the created record:
 
@@ -99,7 +100,7 @@ defmodule Phoenix.Channel do
         end
       end
 
-  Alternatively, you may just want to ack the status of the operation:
+  Or you may just want to confirm that the operation succeeded:
 
       def handle_in("create:post", attrs, socket) do
         changeset = Post.changeset(%Post{}, attrs)
@@ -112,10 +113,53 @@ defmodule Phoenix.Channel do
         end
       end
 
-  Like binary pushes, binary data is also supported with replies via a `{:binary, data}` tuple:
+  Binary data is also supported with replies via a `{:binary, data}` tuple:
 
       {:reply, {:ok, {:binary, bin}}, socket}
 
+  If you don't want to send a reply to the client, you can return:
+
+      {:noreply, socket}
+
+  One situation when you might do this is if you need to reply later; see
+  `reply/2`.
+
+  ## Pushes
+  
+  Calling `push/3` allows you to send a message to the client which is not a
+  reply to a specific client message. Because it is not a reply, a pushed
+  message does not contain a client message `ref`; there is no prior client
+  message to relate it to.
+
+  Possible use cases include notifying a client that:
+  - You've auto-saved the user's document
+  - The user's game is ending soon
+  - The IoT device's settings should be updated
+
+  For example, you could `push/3` a message to the client in `handle_info/3`
+  after receiving a `PubSub` message relevant to them.
+
+      alias Phoenix.Socket.Broadcast
+      def handle_info(%Broadcast{topic: _, event: event, payload: payload}, socket) do
+        push(socket, event, payload)
+        {:noreply, socket}
+      end
+
+  Push data can be given in the form of a map or a tagged `{:binary, data}`
+  tuple:
+
+      # client asks for their current rank. reply contains it, and client
+      # is also pushed a leader board and a badge image
+      def handle_in("current_rank", _, socket) do
+        push(socket, "leaders", %{leaders: Game.get_leaders(socket.assigns.game_id)})
+        push(socket, "badge", {:binary, File.read!(socket.assigns.badge_path)})
+        {:reply, %{val: Game.get_rank(socket.assigns[:user])}, socket}
+      end
+
+  Note that in this example, `push/3` is called from `handle_in/3`; in this way
+  you can essentially reply N times to a single message from the client. See
+  `reply/2` for why this may be desirable.
+
   ## Intercepting Outgoing Events
 
   When an event is broadcasted with `broadcast/3`, each channel subscriber can
@@ -566,11 +610,19 @@ defmodule Phoenix.Channel do
   end
 
   @doc """
-  Sends event to the socket.
+  Sends an event directly to the connected client without requiring a prior
+  message from the client.
 
   The event's message must be a serializable map or a tagged `{:binary, data}`
   tuple where `data` is binary data.
 
+  Note that unlike some in client libraries, this server-side `push/3` does not
+  return a reference. If you need to get a reply from the client and to
+  correlate that reply with the message you pushed, you'll need to include a
+  unique identifier in the message, track it in the Channel's state, have the
+  client include it in its reply, and examine the ref when the reply comes to
+  `handle_in/3`.
+
   ## Examples
 
       iex> push(socket, "new_message", %{id: 1, content: "hello"})
@@ -588,17 +640,27 @@ defmodule Phoenix.Channel do
   @doc """
   Replies asynchronously to a socket push.
 
-  Useful when you need to reply to a push that can't otherwise be handled using
-  the `{:reply, {status, payload}, socket}` return from your `handle_in`
-  callbacks. `reply/2` will be used in the rare cases you need to perform work in
-  another process and reply when finished by generating a reference to the push
-  with `socket_ref/1`.
-
-  *Note*: In such cases, a `socket_ref` should be generated and
-  passed to the external process, so the `socket` itself is not leaked outside
-  the channel. The `socket` holds information such as assigns and transport
-  configuration, so it's important to not copy this information outside of the
-  channel that owns it.
+  The usual way of replying to a client's message is to return a tuple from `handle_in/3`
+  like:
+
+      {:reply, {status, payload}, socket}
+
+  But sometimes you need to reply to a push asynchronously - that is, after
+  your `handle_in/3` callback completes. For example, you might need to perform
+  work in another process and reply when it's finished.
+
+  You can do this by generating a reference to the socket with `socket_ref/1`
+  and calling `reply/2` with that ref when you're ready to reply.
+
+  *Note*: A `socket_ref` is required so the `socket` itself is not leaked
+  outside the channel. The `socket` holds information such as assigns and
+  transport configuration, so it's important to not copy this information
+  outside of the channel that owns it.
+
+  Technically, `reply/2` will allow you to reply multiple times to the same
+  client message, and each reply will include the client message `ref`. But the
+  client may expect only one reply; in that case, `push/3` would be preferable
+  for the additional messages.
 
   ## Examples
 
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index 7eeed8a5f..794a89f4a 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -535,6 +535,14 @@ defmodule Phoenix.VerifiedRoutes do
     |> URI.encode(&URI.char_unreserved?/1)
   end
 
+  # Segments must always start with /
+  defp verify_segment(["/" <> _ | _] = segments, route), do: verify_segment(segments, route, [])
+
+  defp verify_segment(_, route) do
+    raise ArgumentError, "paths must begin with /, got: #{Macro.to_string(route)}"
+  end
+
+  # separator followed by dynamic
   defp verify_segment(["/" | rest], route, acc), do: verify_segment(rest, route, ["/" | acc])
 
   # we've found a static segment, return to caller with rewritten query if found
@@ -561,7 +569,7 @@ defmodule Phoenix.VerifiedRoutes do
 
   defp verify_segment([segment | _], route, _acc) when is_binary(segment) do
     raise ArgumentError,
-          "path segments must begin with /, got: #{inspect(segment)} in #{Macro.to_string(route)}"
+          "path segments after interpolation must begin with /, got: #{inspect(segment)} in #{Macro.to_string(route)}"
   end
 
   defp verify_segment(
@@ -711,7 +719,7 @@ defmodule Phoenix.VerifiedRoutes do
 
   defp rewrite_path(route, endpoint, router, statics) do
     {:<<>>, meta, segments} = route
-    {path_rewrite, query_rewrite} = verify_segment(segments, route, [])
+    {path_rewrite, query_rewrite} = verify_segment(segments, route)
 
     rewrite_route =
       quote generated: true do


Test patch:
diff --git a/guides/testing/testing.md b/guides/testing/testing.md
index 136d40f9a..2e85cb311 100644
--- a/guides/testing/testing.md
+++ b/guides/testing/testing.md
@@ -145,7 +145,7 @@ The error view test case, `test/hello_web/controllers/error_html_test.exs`, illu
 defmodule HelloWeb.ErrorHTMLTest do
   use HelloWeb.ConnCase, async: true
 
-  # Bring render_to_string/3 for testing custom views
+  # Bring render_to_string/4 for testing custom views
   import Phoenix.Template
 
   test "renders 404.html" do
@@ -337,18 +337,18 @@ defmodule HelloWeb.ErrorHTMLTest do
 
   @moduletag :error_view_case
 
-  # Bring render/3 and render_to_string/3 for testing custom views
-  import Phoenix.View
+  # Bring render/4 and render_to_string/4 for testing custom views
+  import Phoenix.Template
 
   @tag individual_test: "yup"
   test "renders 404.html" do
-    assert render_to_string(HelloWeb.ErrorView, "404.html", []) ==
+    assert render_to_string(HelloWeb.ErrorView, "404", "html", []) ==
            "Not Found"
   end
 
   @tag individual_test: "nope"
   test "renders 500.html" do
-    assert render_to_string(HelloWeb.ErrorView, "500.html", []) ==
+    assert render_to_string(HelloWeb.ErrorView, "500", "html", []) ==
            "Internal Server Error"
   end
 end
diff --git a/installer/templates/phx_test/controllers/error_html_test.exs b/installer/templates/phx_test/controllers/error_html_test.exs
index 965e5892e..6d3396470 100644
--- a/installer/templates/phx_test/controllers/error_html_test.exs
+++ b/installer/templates/phx_test/controllers/error_html_test.exs
@@ -1,7 +1,7 @@
 defmodule <%= @web_namespace %>.ErrorHTMLTest do
   use <%= @web_namespace %>.ConnCase, async: true
 
-  # Bring render_to_string/3 for testing custom views
+  # Bring render_to_string/4 for testing custom views
   import Phoenix.Template
 
   test "renders 404.html" do
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 558c3e796..53e5d3b7b 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -201,7 +201,7 @@ defmodule Phoenix.VerifiedRoutesTest do
 
   test "~p raises when not prefixed by /" do
     assert_raise ArgumentError,
-                 ~s|path segments must begin with /, got: "posts/1" in "posts/1"|,
+                 ~s|paths must begin with /, got: "posts/1"|,
                  fn ->
                    defmodule SigilPPrefix do
                      use Phoenix.VerifiedRoutes,