From c3fff015432187a729fd0df9ad079346692e5ffc Mon Sep 17 00:00:00 2001 From: Martin Karrer Date: Thu, 15 Jan 2026 22:40:04 +0100 Subject: [PATCH 1/4] Remove Ecto references from AGENTS.md for --no-ecto projects --- installer/test/phx_new_test.exs | 6 ++++++ usage-rules/ecto.md | 23 +++++++++++++++++++++++ usage-rules/elixir.md | 2 +- usage-rules/liveview.md | 30 +++--------------------------- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index 7df19fce61..8d11f83c55 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -555,6 +555,12 @@ defmodule Mix.Tasks.Phx.NewTest do assert file =~ "inputs: [\"*.{heex,ex,exs}\", \"{config,lib,test}/**/*.{heex,ex,exs}\"]" refute file =~ "subdirectories:" end) + + assert_file("phx_blog/AGENTS.md", fn file -> + refute file =~ "Ecto" + refute file =~ "changeset" + refute file =~ "Ecto.Schema" + end) end) end diff --git a/usage-rules/ecto.md b/usage-rules/ecto.md index f42bbdb903..ec259aa5a4 100644 --- a/usage-rules/ecto.md +++ b/usage-rules/ecto.md @@ -7,3 +7,26 @@ - You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields - Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct - **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied + +### Creating LiveView forms from changesets + +When using changesets with LiveView, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: + + defmodule MyApp.Users.User do + use Ecto.Schema + ... + end + +And then you create a changeset that you pass to `to_form`: + + %MyApp.Users.User{} + |> Ecto.Changeset.change() + |> to_form() + +Once the form is submitted, the params will be available under `%{"user" => user_params}`. + +In the template, the form assign can be passed to the `<.form>` function component: + + <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:field]} type="text" /> + diff --git a/usage-rules/elixir.md b/usage-rules/elixir.md index 63af87f1ad..5cbbdbdfab 100644 --- a/usage-rules/elixir.md +++ b/usage-rules/elixir.md @@ -29,7 +29,7 @@ end - **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors -- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets +- **Never** use map access syntax on structs as they do not implement the Access behaviour by default. For structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist - Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) - Don't use `String.to_atom/1` on user input (memory leak risk) - Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards diff --git a/usage-rules/liveview.md b/usage-rules/liveview.md index 483ad546a3..2595f28910 100644 --- a/usage-rules/liveview.md +++ b/usage-rules/liveview.md @@ -186,29 +186,6 @@ You can also specify a name to nest the params: {:noreply, assign(socket, form: to_form(user_params, as: :user))} end -#### Creating a form from changesets - -When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: - - defmodule MyApp.Users.User do - use Ecto.Schema - ... - end - -And then you create a changeset that you pass to `to_form`: - - %MyApp.Users.User{} - |> Ecto.Changeset.change() - |> to_form() - -Once the form is submitted, the params will be available under `%{"user" => user_params}`. - -In the template, the form form assign can be passed to the `<.form>` function component: - - <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> - <.input field={@form[:field]} type="text" /> - - Always give the form an explicit, unique DOM ID, like `id="todo-form"`. #### Avoiding form errors @@ -223,9 +200,8 @@ Always give the form an explicit, unique DOM ID, like `id="todo-form"`. And **never** do this: <%!-- NEVER do this (invalid) --%> - <.form for={@changeset} id="my-form"> - <.input field={@changeset[:field]} type="text" /> + <.form for={@data} id="my-form"> + <.input field={@data[:field]} type="text" /> -- You are FORBIDDEN from accessing the changeset in the template as it will cause errors -- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset +- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/1` or `to_form/2` assigned in the LiveView module From 95c4c8b22b7194a512960ba1c7ba03c3dcf2015a Mon Sep 17 00:00:00 2001 From: Martin Karrer Date: Mon, 19 Jan 2026 14:47:28 +0100 Subject: [PATCH 2/4] Isolate AGENTS.md content for all phx.new flags --- installer/lib/phx_new/generator.ex | 9 ++++ .../templates/usage-rules/phoenix-html.md | 6 +++ .../templates/usage-rules/phoenix-live.md | 6 +++ installer/templates/usage-rules/phoenix.md | 8 ---- installer/test/phx_new_test.exs | 35 +++++++++++++++ usage-rules/ecto-forms.md | 45 +++++++++++++++++++ usage-rules/ecto.md | 2 +- usage-rules/elixir.md | 12 ++--- usage-rules/html.md | 4 +- usage-rules/liveview.md | 18 ++------ 10 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 installer/templates/usage-rules/phoenix-html.md create mode 100644 installer/templates/usage-rules/phoenix-live.md create mode 100644 usage-rules/ecto-forms.md diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex index 76d76c3e64..3c30d38450 100644 --- a/installer/lib/phx_new/generator.ex +++ b/installer/lib/phx_new/generator.ex @@ -123,6 +123,8 @@ defmodule Phx.New.Generator do # rules specific to new apps @new_project_rules_files["project.md"], @new_project_rules_files["phoenix.md"], + project.binding[:html] && @new_project_rules_files["phoenix-html.md"], + project.binding[:live] && @new_project_rules_files["phoenix-live.md"], # --no-assets is equivalent to --no-tailwind && --no-esbuild; # we check for both here project.binding[:javascript] && project.binding[:css] && @@ -157,6 +159,13 @@ defmodule Phx.New.Generator do @rules_files["liveview.md"], "\n" ], + # Include ecto-forms when both ecto and html are present + project.binding[:ecto] && project.binding[:html] && + [ + "\n", + @rules_files["ecto-forms.md"], + "\n" + ], "" ] |> Enum.reject(fn part -> part == nil or part == false end) diff --git a/installer/templates/usage-rules/phoenix-html.md b/installer/templates/usage-rules/phoenix-html.md new file mode 100644 index 0000000000..0e59db444c --- /dev/null +++ b/installer/templates/usage-rules/phoenix-html.md @@ -0,0 +1,6 @@ +### Phoenix Components + +- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar +- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors +- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your +custom classes must fully style the input diff --git a/installer/templates/usage-rules/phoenix-live.md b/installer/templates/usage-rules/phoenix-live.md new file mode 100644 index 0000000000..b8d230b80e --- /dev/null +++ b/installer/templates/usage-rules/phoenix-live.md @@ -0,0 +1,6 @@ +### Phoenix LiveView Layout Guidelines + +- **Always** begin your LiveView templates with `` which wraps all inner content +- Anytime you run into errors with no `current_scope` assign: + - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `` + - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed diff --git a/installer/templates/usage-rules/phoenix.md b/installer/templates/usage-rules/phoenix.md index 8af1195b40..47443d5431 100644 --- a/installer/templates/usage-rules/phoenix.md +++ b/installer/templates/usage-rules/phoenix.md @@ -1,12 +1,4 @@ ### Phoenix v1.8 guidelines -- **Always** begin your LiveView templates with `` which wraps all inner content - The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again -- Anytime you run into errors with no `current_scope` assign: - - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `` - - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed - Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module -- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar -- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors -- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your -custom classes must fully style the input diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index 8d11f83c55..7d2e9ce38a 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -518,6 +518,40 @@ defmodule Mix.Tasks.Phx.NewTest do assert_file("phx_blog/config/test.exs", fn file -> refute file =~ ~s|config :phoenix_live_view| end) + + assert_file("phx_blog/AGENTS.md", fn file -> + refute file =~ "<.form" + refute file =~ "<.input" + refute file =~ "LiveView" + end) + end) + end + + test "new with --no-live" do + in_tmp("new with no_live", fn -> + Mix.Tasks.Phx.New.run([@app_name, "--no-live"]) + + assert_file("phx_blog/AGENTS.md", fn file -> + refute file =~ "## Phoenix LiveView guidelines" + refute file =~ "LiveView streams" + refute file =~ "push_event" + refute file =~ "handle_event" + refute file =~ "phx-hook" + end) + end) + end + + test "new with --no-ecto --no-live" do + in_tmp("new with no_ecto and no_live", fn -> + Mix.Tasks.Phx.New.run([@app_name, "--no-ecto", "--no-live"]) + + assert_file("phx_blog/AGENTS.md", fn file -> + refute file =~ "Ecto" + refute file =~ "changeset" + refute file =~ "## Phoenix LiveView guidelines" + refute file =~ "LiveView streams" + refute file =~ "phx-hook" + end) end) end @@ -560,6 +594,7 @@ defmodule Mix.Tasks.Phx.NewTest do refute file =~ "Ecto" refute file =~ "changeset" refute file =~ "Ecto.Schema" + refute file =~ "Ecto.Changeset" end) end) end diff --git a/usage-rules/ecto-forms.md b/usage-rules/ecto-forms.md new file mode 100644 index 0000000000..62e8bf6b4e --- /dev/null +++ b/usage-rules/ecto-forms.md @@ -0,0 +1,45 @@ +## Ecto Forms + +### Creating forms from changesets + +When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: + + defmodule MyApp.Users.User do + use Ecto.Schema + ... + end + +And then you create a changeset that you pass to `to_form`: + + %MyApp.Users.User{} + |> Ecto.Changeset.change() + |> to_form() + +Once the form is submitted, the params will be available under `%{"user" => user_params}`. + +In the template, the form assign can be passed to the `<.form>` function component: + + <.form for={@form} id="todo-form" phx-submit="save"> + <.input field={@form[:field]} type="text" /> + + +Always give the form an explicit, unique DOM ID, like `id="todo-form"`. + +### Avoiding form errors with changesets + +**Always** use a form assigned via `to_form/1` or `to_form/2`, and the `<.input>` component in the template. In the template **always access forms this way**: + + <%!-- ALWAYS do this (valid) --%> + <.form for={@form} id="my-form"> + <.input field={@form[:field]} type="text" /> + + +And **never** do this: + + <%!-- NEVER do this (invalid) --%> + <.form for={@changeset} id="my-form"> + <.input field={@changeset[:field]} type="text" /> + + +- You are FORBIDDEN from accessing the changeset in the template as it will cause errors +- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/1` or `to_form/2` that is derived from a changeset diff --git a/usage-rules/ecto.md b/usage-rules/ecto.md index ec259aa5a4..e67c7e3295 100644 --- a/usage-rules/ecto.md +++ b/usage-rules/ecto.md @@ -4,7 +4,7 @@ - Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` - `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` - `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed -- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields +- **Never** use map access syntax (`changeset[:field]`) on changesets. You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields - Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct - **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied diff --git a/usage-rules/elixir.md b/usage-rules/elixir.md index 5cbbdbdfab..508543adca 100644 --- a/usage-rules/elixir.md +++ b/usage-rules/elixir.md @@ -18,18 +18,18 @@ you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: # INVALID: we are rebinding inside the `if` and the result never gets assigned - if connected?(socket) do - socket = assign(socket, :val, val) + if some_condition?(data) do + data = transform(data) end # VALID: we rebind the result of the `if` to a new variable - socket = - if connected?(socket) do - assign(socket, :val, val) + data = + if some_condition?(data) do + transform(data) end - **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors -- **Never** use map access syntax on structs as they do not implement the Access behaviour by default. For structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist +- **Never** use map access syntax (`my_struct[:field]`) on structs as they do not implement the Access behaviour by default. For structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist - Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) - Don't use `String.to_atom/1` on user input (memory leak risk) - Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards diff --git a/usage-rules/html.md b/usage-rules/html.md index e47e79648f..c48c1447be 100644 --- a/usage-rules/html.md +++ b/usage-rules/html.md @@ -2,9 +2,9 @@ - Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` - **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated -- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` +- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(:form, to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` - **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`) -- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) +- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all templates and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) - Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals. diff --git a/usage-rules/liveview.md b/usage-rules/liveview.md index 2595f28910..e1ff80bc68 100644 --- a/usage-rules/liveview.md +++ b/usage-rules/liveview.md @@ -186,22 +186,10 @@ You can also specify a name to nest the params: {:noreply, assign(socket, form: to_form(user_params, as: :user))} end -Always give the form an explicit, unique DOM ID, like `id="todo-form"`. - -#### Avoiding form errors +In the template, the form assign can be passed to the `<.form>` function component: -**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**: - - <%!-- ALWAYS do this (valid) --%> - <.form for={@form} id="my-form"> + <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> <.input field={@form[:field]} type="text" /> -And **never** do this: - - <%!-- NEVER do this (invalid) --%> - <.form for={@data} id="my-form"> - <.input field={@data[:field]} type="text" /> - - -- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/1` or `to_form/2` assigned in the LiveView module +Always give the form an explicit, unique DOM ID, like `id="todo-form"`. From adf95a7550912e3c3373cacf6ae6d6f608f30e51 Mon Sep 17 00:00:00 2001 From: Martin Karrer Date: Mon, 19 Jan 2026 15:02:11 +0100 Subject: [PATCH 3/4] Fix LiveView leakage in --no-live projects with Ecto --- installer/lib/phx_new/generator.ex | 11 ++++++-- usage-rules/ecto-forms.md | 4 +-- usage-rules/ecto-live-forms.md | 45 ++++++++++++++++++++++++++++++ usage-rules/ecto.md | 23 --------------- 4 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 usage-rules/ecto-live-forms.md diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex index 3c30d38450..7aa60078aa 100644 --- a/installer/lib/phx_new/generator.ex +++ b/installer/lib/phx_new/generator.ex @@ -159,13 +159,20 @@ defmodule Phx.New.Generator do @rules_files["liveview.md"], "\n" ], - # Include ecto-forms when both ecto and html are present - project.binding[:ecto] && project.binding[:html] && + # Include ecto-forms when ecto and html are present, but NOT live + project.binding[:ecto] && project.binding[:html] && !project.binding[:live] && [ "\n", @rules_files["ecto-forms.md"], "\n" ], + # Include ecto-live-forms when both ecto and live are present + project.binding[:ecto] && project.binding[:live] && + [ + "\n", + @rules_files["ecto-live-forms.md"], + "\n" + ], "" ] |> Enum.reject(fn part -> part == nil or part == false end) diff --git a/usage-rules/ecto-forms.md b/usage-rules/ecto-forms.md index 62e8bf6b4e..fa93c2597c 100644 --- a/usage-rules/ecto-forms.md +++ b/usage-rules/ecto-forms.md @@ -19,7 +19,7 @@ Once the form is submitted, the params will be available under `%{"user" => user In the template, the form assign can be passed to the `<.form>` function component: - <.form for={@form} id="todo-form" phx-submit="save"> + <.form for={@form} id="todo-form"> <.input field={@form[:field]} type="text" /> @@ -42,4 +42,4 @@ And **never** do this: - You are FORBIDDEN from accessing the changeset in the template as it will cause errors -- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/1` or `to_form/2` that is derived from a changeset +- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]` diff --git a/usage-rules/ecto-live-forms.md b/usage-rules/ecto-live-forms.md new file mode 100644 index 0000000000..8d03356875 --- /dev/null +++ b/usage-rules/ecto-live-forms.md @@ -0,0 +1,45 @@ +## Ecto LiveView Forms + +### Creating LiveView forms from changesets + +When using changesets with LiveView, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: + + defmodule MyApp.Users.User do + use Ecto.Schema + ... + end + +And then you create a changeset that you pass to `to_form`: + + %MyApp.Users.User{} + |> Ecto.Changeset.change() + |> to_form() + +Once the form is submitted, the params will be available under `%{"user" => user_params}`. + +In the template, the form assign can be passed to the `<.form>` function component: + + <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:field]} type="text" /> + + +Always give the form an explicit, unique DOM ID, like `id="todo-form"`. + +### Avoiding form errors with changesets + +**Always** use a form assigned via `to_form/1` or `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this way**: + + <%!-- ALWAYS do this (valid) --%> + <.form for={@form} id="my-form"> + <.input field={@form[:field]} type="text" /> + + +And **never** do this: + + <%!-- NEVER do this (invalid) --%> + <.form for={@changeset} id="my-form"> + <.input field={@changeset[:field]} type="text" /> + + +- You are FORBIDDEN from accessing the changeset in the template as it will cause errors +- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]` diff --git a/usage-rules/ecto.md b/usage-rules/ecto.md index e67c7e3295..f19a52802f 100644 --- a/usage-rules/ecto.md +++ b/usage-rules/ecto.md @@ -7,26 +7,3 @@ - **Never** use map access syntax (`changeset[:field]`) on changesets. You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields - Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct - **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied - -### Creating LiveView forms from changesets - -When using changesets with LiveView, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: - - defmodule MyApp.Users.User do - use Ecto.Schema - ... - end - -And then you create a changeset that you pass to `to_form`: - - %MyApp.Users.User{} - |> Ecto.Changeset.change() - |> to_form() - -Once the form is submitted, the params will be available under `%{"user" => user_params}`. - -In the template, the form assign can be passed to the `<.form>` function component: - - <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> - <.input field={@form[:field]} type="text" /> - From e04da94988e848a02eddf694bea5b61508df5f6f Mon Sep 17 00:00:00 2001 From: Martin Karrer Date: Mon, 19 Jan 2026 15:16:40 +0100 Subject: [PATCH 4/4] Remove UI/CSS guidelines from API-only projects --- installer/lib/phx_new/generator.ex | 5 +++-- installer/templates/usage-rules/phoenix-ui.md | 4 ++++ installer/templates/usage-rules/phoenix.md | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 installer/templates/usage-rules/phoenix-ui.md diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex index 7aa60078aa..90ebfe9560 100644 --- a/installer/lib/phx_new/generator.ex +++ b/installer/lib/phx_new/generator.ex @@ -123,11 +123,12 @@ defmodule Phx.New.Generator do # rules specific to new apps @new_project_rules_files["project.md"], @new_project_rules_files["phoenix.md"], + project.binding[:html] && @new_project_rules_files["phoenix-ui.md"], project.binding[:html] && @new_project_rules_files["phoenix-html.md"], project.binding[:live] && @new_project_rules_files["phoenix-live.md"], # --no-assets is equivalent to --no-tailwind && --no-esbuild; - # we check for both here - project.binding[:javascript] && project.binding[:css] && + # Only include assets.md for HTML projects (not API-only) + project.binding[:html] && project.binding[:javascript] && project.binding[:css] && @new_project_rules_files["assets.md"], # generic usage rules "\n", diff --git a/installer/templates/usage-rules/phoenix-ui.md b/installer/templates/usage-rules/phoenix-ui.md new file mode 100644 index 0000000000..74ed0efc1c --- /dev/null +++ b/installer/templates/usage-rules/phoenix-ui.md @@ -0,0 +1,4 @@ +### Phoenix Layouts & Flash + +- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again +- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module diff --git a/installer/templates/usage-rules/phoenix.md b/installer/templates/usage-rules/phoenix.md index 47443d5431..037fc0944e 100644 --- a/installer/templates/usage-rules/phoenix.md +++ b/installer/templates/usage-rules/phoenix.md @@ -1,4 +1,3 @@ ### Phoenix v1.8 guidelines -- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again -- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module +This section intentionally minimal - core Phoenix concepts are covered in the main usage-rules sections below.