Repository: phoenixframework/phoenix
Pull request #4977 (2022-09-25 01:57:25)

Update select tag generator with new components syntax
This fixes two issues when generating HTML for a schema with a select tag.

The first is the slot within a `for` comprehension.

The other is the missing attribute `value` for the `:option` slot:
```
warning: undefined attribute "value" in slot "option" for component PhxBlogWeb.Components.input/1
  lib/phx_blog_web/templates/post/edit.html.heex:17: (file)
```

leandrocp: For reference, that previous code would fail at phx.gen tests like: https://github.com/phoenixframework/phoenix/blob/64ab5740a4c69337590a3c859ed52ac0744741a4/integration_test/test/code_generation/app_with_defaults_test.exs#L30

I'll get https://github.com/phoenixframework/phoenix/pull/4820 ready for review soon so we can test all templates.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc66afe19..6c1657dd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
 
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Set up Elixir
       uses: erlef/setup-beam@v1
@@ -34,7 +34,7 @@ jobs:
         otp-version: ${{ matrix.otp }}
 
     - name: Restore deps and _build cache
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: |
           deps
@@ -67,10 +67,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Restore deps and _build cache
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: |
             deps
@@ -80,12 +80,12 @@ jobs:
             deps-${{ runner.os }}-npm
 
       - name: Set up Node.js 12.x
-        uses: actions/setup-node@v1
+        uses: actions/setup-node@v3
         with:
           node-version: 12.x
 
       - name: Restore npm cache
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ~/.npm
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5d4f32d96..7696930c4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -225,3 +225,4 @@ For formatting the guides:
   in the template.
 - We use backticks for filenames and directory paths.
 - We use backticks for module names, function names, and variable names.
+- Documentation line length should hard wrapped at around 100 characters if possible.
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 5cb8832aa..db8a2f09f 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -40,7 +40,7 @@ config :esbuild,
 
 # Configure tailwind (the version is required)
 config :tailwind,
-  version: "3.1.0",
+  version: "3.1.8",
   default: [
     args: ~w(
       --config=tailwind.config.js
diff --git a/installer/templates/phx_web/components.ex b/installer/templates/phx_web/components.ex
index 897d4fe7f..2b304be2d 100644
--- a/installer/templates/phx_web/components.ex
+++ b/installer/templates/phx_web/components.ex
@@ -24,7 +24,7 @@ defmodule <%= @web_namespace %>.Components do
         Are you sure?
         <:confirm>OK</:confirm>
         <:cancel>Cancel</:cancel>
-      <.modal>
+      </.modal>
 
   JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
   for the caller to reactor to each button press, for example:
@@ -32,8 +32,8 @@ defmodule <%= @web_namespace %>.Components do
       <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
         Are you sure you?
         <:confirm>OK</:confirm>
-        <:cancel>Cancel</:confirm>
-      <.modal>
+        <:cancel>Cancel</:cancel>
+      </.modal>
   """
   attr :id, :string, required: true
   attr :show, :boolean, default: false
@@ -126,10 +126,10 @@ defmodule <%= @web_namespace %>.Components do
   attr :id, :string, default: "flash", doc: "the optional id of flash container"
   attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
   attr :title, :string, default: nil
-  attr :rest, :global
-  attr :kind, :atom, doc: "one of :info, :error used for styling and flash lookup"
+  attr :kind, :atom, values: [:info, :error], doc: "Used for styling and flash lookup"
   attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
   attr :close, :boolean, default: true, doc: "whether the flash can be closed"
+  attr :rest, :global, doc: "the aribtrary HTML attributes to add to the flash container"
 
   slot :inner_block, doc: "the optional inner block that renders the flash message"
 
@@ -142,7 +142,7 @@ defmodule <%= @web_namespace %>.Components do
       phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#flash")}
       role="alert"
       class={[
-        "fixed hidden top-2 right-2 w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
+        "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
         @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
         @kind == :error && "bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
       ]}
@@ -171,12 +171,14 @@ defmodule <%= @web_namespace %>.Components do
         <.input field={{f, :username}} label="Username" />
         <:actions>
           <.button>Save</.button>
-        <:actions>
+        </:actions>
       </.simple_form>
   """
   attr :for, :any, default: nil, doc: "the datastructure for the form"
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
-  attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the form tag"
+  attr :rest, :global,
+    include: ~w(autocomplete name rel action enctype method novalidate target),
+    doc: "the arbitrary HTML attributes to apply to the form tag"
 
   slot :inner_block, required: true
   slot :actions, doc: "the slot for form actions, such as a submit button"
@@ -204,7 +206,7 @@ defmodule <%= @web_namespace %>.Components do
   """
   attr :type, :string, default: nil
   attr :class, :string, default: nil
-  attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the button tag"
+  attr :rest, :global, include: ~w(disabled form name value)
 
   slot :inner_block, required: true
 
@@ -242,15 +244,18 @@ defmodule <%= @web_namespace %>.Components do
 
   attr :type, :string,
     default: "text",
-    doc: ~s|one of "text", "textarea", "number" "email", "date", "time", "datetime", "select"|
+    values: ~w(checkbox color date datetime-local email file hidden month number password
+               range radio search tel text time url week)
 
   attr :value, :any
   attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
   attr :errors, :list
-  attr :rest, :global, doc: "the arbitrary HTML attributes for the input tag"
-
+  attr :rest, :global, include: ~w(autocomplete checked disabled form max maxlength min minlength
+                                   multiple pattern placeholder readonly required size step)
   slot :inner_block
-  slot :option, doc: "the slot for select input options"
+  slot :option, doc: "the slot for select input options" do
+    attr :value, :any
+  end
 
   def input(%{field: {f, field}} = assigns) do
     assigns
@@ -283,7 +288,6 @@ defmodule <%= @web_namespace %>.Components do
       <select
         id={@id}
         name={@name}
-        autocomplete={@name}
         class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-zinc-500 focus:border-zinc-500 sm:text-sm"
         {@rest}
       >
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 62e10d498..8710d2c37 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -195,9 +195,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
         ~s"""
         <.input field={{f, #{inspect(key)}}} type="select" label="#{label(key)}">
           <:option>Choose a value</:option>
-          <%= for value <- Ecto.Enum.values(#{inspect(schema.module)}, #{inspect(key)}) do %>
-            <:option value={value}><%= value %></:option>
-          <% end %>
+          <:option :for={value <- Ecto.Enum.values(#{inspect(schema.module)}, #{inspect(key)})} value={value}>
+            <%= value %>
+          </:option>
         </.input>
         """
       {key, _}  ->
diff --git a/lib/phoenix/endpoint/cowboy2_handler.ex b/lib/phoenix/endpoint/cowboy2_handler.ex
index cbedb17a8..727f944f2 100644
--- a/lib/phoenix/endpoint/cowboy2_handler.ex
+++ b/lib/phoenix/endpoint/cowboy2_handler.ex
@@ -24,25 +24,16 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do
               cowboy_opts =
                 opts
                 |> Enum.flat_map(fn
-                  {:timeout, timeout} ->
-                    [idle_timeout: timeout]
-
-                  {:compress, _} = opt ->
-                    [opt]
-
-                  {:max_frame_size, _} = opt ->
-                    [opt]
-
-                  {:fullsweep_after, value} ->
-                    :erlang.process_flag(:fullsweep_after, value)
-                    []
-
-                  _other ->
-                    []
+                  {:timeout, timeout} -> [idle_timeout: timeout]
+                  {:compress, _} = opt -> [opt]
+                  {:max_frame_size, _} = opt -> [opt]
+                  _other -> []
                 end)
                 |> Map.new()
 
-              {:cowboy_websocket, copy_resp_headers(conn, req), [handler | state], cowboy_opts}
+              handler_opts = Keyword.take(opts, [:fullsweep_after])
+              triplet = {handler, handler_opts, state}
+              {:cowboy_websocket, copy_resp_headers(conn, req), triplet, cowboy_opts}
 
             {:error, %Plug.Conn{adapter: {@connection, req}} = conn} ->
               {:ok, copy_resp_headers(conn, req), {handler, opts}}
@@ -136,7 +127,11 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do
 
   ## Websocket callbacks
 
-  def websocket_init([handler | state]) do
+  def websocket_init({handler, process_flags, state}) do
+    for {key, value} <- process_flags do
+      :erlang.process_flag(key, value)
+    end
+
     {:ok, state} = handler.init(state)
     {:ok, [handler | state]}
   end
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index 9af3117ba..65d2138c0 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -188,7 +188,7 @@ defmodule Phoenix.VerifiedRoutes do
       redirect(to: ~p"/users/#{@user}")
 
       ~H"""
-      <.link to={~p"/users??page=#{@page}"}>profile</.link>
+      <.link to={~p"/users?page=#{@page}"}>profile</.link>
 
       <.link to={~p"/users?#{@params}"}>profile</.link>
       """
@@ -642,7 +642,7 @@ defmodule Phoenix.VerifiedRoutes do
   end
 
   @doc false
-  def __encode_query__(dict) when is_list(dict) or is_map(dict) do
+  def __encode_query__(dict) when is_list(dict) or (is_map(dict) and not is_struct(dict)) do
     case Plug.Conn.Query.encode(dict, &to_param/1) do
       "" -> ""
       query_str -> query_str
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index ef263479e..9cd20070e 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -1,6 +1,6 @@
 <.header>Confirm account</.header>
 
-<.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirmation/#{@token}"}>
+<.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
   <:actions>
     <.button>Confirm my account</.button>
   </:actions>
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 544d9d3df..f9924f2db 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -32,7 +32,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       {:ok, _} ->
         {:noreply,
          socket
-         |> put_flash(:info, "User confirmed successfully.")
+         |> put_flash(:info, "<%= inspect schema.alias %> confirmed successfully.")
          |> redirect(to: ~p"/")}
 
       :error ->
@@ -47,7 +47,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           %{} ->
             {:noreply,
              socket
-             |> put_flash(:error, "User confirmation link is invalid or it has expired.")
+             |> put_flash(:error, "<%= inspect schema.alias %> confirmation link is invalid or it has expired.")
              |> redirect(to: ~p"/")}
         end
     end
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 76c559127..605f7272b 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -18,9 +18,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       <.simple_form
         :let={f}
         id="login_form"
-        for={:user}
+        for={:<%= schema.singular %>}
         action={~p"<%= schema.route_prefix %>/log_in"}
-        as={:user}
+        as={:<%= schema.singular %>}
         phx-update="ignore"
       >
         <.input field={{f, :email}} type="email" label="Email" required />
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index b23e4e103..423d00511 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -27,7 +27,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         phx-trigger-action={@trigger_submit}
         action={~p"<%= schema.route_prefix %>/log_in?_action=registered"}
         method="post"
-        as={:user}
+        as={:<%= schema.singular %>}
       >
         <%%= if @changeset.action == :insert do %>
           <.error message="Oops, something went wrong! Please check the errors below." />
diff --git a/priv/templates/phx.gen.live/components.ex b/priv/templates/phx.gen.live/components.ex
index 897d4fe7f..33ddb665b 100644
--- a/priv/templates/phx.gen.live/components.ex
+++ b/priv/templates/phx.gen.live/components.ex
@@ -24,7 +24,7 @@ defmodule <%= @web_namespace %>.Components do
         Are you sure?
         <:confirm>OK</:confirm>
         <:cancel>Cancel</:cancel>
-      <.modal>
+      </.modal>
 
   JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
   for the caller to reactor to each button press, for example:
@@ -32,8 +32,8 @@ defmodule <%= @web_namespace %>.Components do
       <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
         Are you sure you?
         <:confirm>OK</:confirm>
-        <:cancel>Cancel</:confirm>
-      <.modal>
+        <:cancel>Cancel</:cancel>
+      </.modal>
   """
   attr :id, :string, required: true
   attr :show, :boolean, default: false
@@ -171,7 +171,7 @@ defmodule <%= @web_namespace %>.Components do
         <.input field={{f, :username}} label="Username" />
         <:actions>
           <.button>Save</.button>
-        <:actions>
+        </:actions>
       </.simple_form>
   """
   attr :for, :any, default: nil, doc: "the datastructure for the form"
@@ -250,7 +250,9 @@ defmodule <%= @web_namespace %>.Components do
   attr :rest, :global, doc: "the arbitrary HTML attributes for the input tag"
 
   slot :inner_block
-  slot :option, doc: "the slot for select input options"
+  slot :option, doc: "the slot for select input options" do
+    attr :value, :any
+  end
 
   def input(%{field: {f, field}} = assigns) do
     assigns


Test patch:
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index 75b907e14..22abeb500 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -33,7 +33,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, conn} = result
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-        "User confirmed successfully"
+        "<%= inspect schema.alias %> confirmed successfully"
 
       assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
       refute get_session(conn, :<%= schema.singular %>_token)
@@ -50,7 +50,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, conn} = result
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-        "User confirmation link is invalid or it has expired"
+        "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
 
 
       # when logged in
@@ -81,7 +81,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         |> follow_redirect(conn, ~p"/")
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-        "User confirmation link is invalid or it has expired"
+        "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
 
       refute <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
     end
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 4d10a41ac..b2d29e060 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -125,6 +125,10 @@ defmodule Phoenix.VerifiedRoutesTest do
 
     assert path(@endpoint, @router, ~p"/posts/bottom/#{dir}/#{id}?foo=bar") ==
              "/posts/bottom/asc/123?foo=bar"
+
+    # dynamic query params
+    assert ~p"/posts/1?other_post=#{id}" == "/posts/1?other_post=123"
+    assert ~p"/posts/1?other_post=#{struct}" == "/posts/1?other_post=post-123"
   end
 
   test "~p with dynamic string and static query params" do