diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex index 76d76c3e64..90ebfe9560 100644 --- a/installer/lib/phx_new/generator.ex +++ b/installer/lib/phx_new/generator.ex @@ -123,9 +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", @@ -157,6 +160,20 @@ defmodule Phx.New.Generator do @rules_files["liveview.md"], "\n" ], + # 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/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-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 8af1195b40..037fc0944e 100644 --- a/installer/templates/usage-rules/phoenix.md +++ b/installer/templates/usage-rules/phoenix.md @@ -1,12 +1,3 @@ ### 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 +This section intentionally minimal - core Phoenix concepts are covered in the main usage-rules sections below. diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index 7df19fce61..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 @@ -555,6 +589,13 @@ 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" + 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..fa93c2597c --- /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"> + <.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]` 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 f42bbdb903..f19a52802f 100644 --- a/usage-rules/ecto.md +++ b/usage-rules/ecto.md @@ -4,6 +4,6 @@ - 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 63af87f1ad..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 (`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 (`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 483ad546a3..e1ff80bc68 100644 --- a/usage-rules/liveview.md +++ b/usage-rules/liveview.md @@ -186,46 +186,10 @@ 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: +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 - -**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"> - <.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/2` assigned in the LiveView module that is derived from a changeset