daily-tracker

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

AGENTS.md (26186B)


      1 This is a web application written using the Phoenix web framework.
      2 
      3 ## Project guidelines
      4 
      5 - Use `mix precommit` alias when you are done with all changes and fix any pending issues
      6 - Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps
      7 
      8 ### Phoenix v1.8 guidelines
      9 
     10 - **Always** begin your LiveView templates with `<Layouts.app flash={@flash} ...>` which wraps all inner content
     11 - The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again
     12 - Anytime you run into errors with no `current_scope` assign:
     13   - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `<Layouts.app>`
     14   - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed
     15 - 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
     16 - 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
     17 - **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
     18 - 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
     19 custom classes must fully style the input
     20 
     21 ### JS and CSS guidelines
     22 
     23 - **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces.
     24 - Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`:
     25 
     26       @import "tailwindcss" source(none);
     27       @source "../css";
     28       @source "../js";
     29       @source "../../lib/my_app_web";
     30 
     31 - **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new`
     32 - **Never** use `@apply` when writing raw css
     33 - **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design
     34 - Out of the box **only the app.js and app.css bundles are supported**
     35   - You cannot reference an external vendor'd script `src` or link `href` in the layouts
     36   - You must import the vendor deps into app.js and app.css to use them
     37   - **Never write inline <script>custom js</script> tags within templates**
     38 
     39 ### UI/UX & design guidelines
     40 
     41 - **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles
     42 - Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions)
     43 - Ensure **clean typography, spacing, and layout balance** for a refined, premium look
     44 - Focus on **delightful details** like hover effects, loading states, and smooth page transitions
     45 
     46 
     47 <!-- phoenix-gen-auth-start -->
     48 ## Authentication
     49 
     50 - **Always** handle authentication flow at the router level with proper redirects
     51 - **Always** be mindful of where to place routes. `phx.gen.auth` creates multiple router plugs and `live_session` scopes:
     52   - A plug `:fetch_current_scope_for_user` that is included in the default browser pipeline
     53   - A plug `:require_authenticated_user` that redirects to the log in page when the user is not authenticated
     54   - A `live_session :current_user` scope - for routes that need the current user but don't require authentication, similar to `:fetch_current_scope_for_user`
     55   - A `live_session :require_authenticated_user` scope - for routes that require authentication, similar to the plug with the same name
     56   - In both cases, a `@current_scope` is assigned to the Plug connection and LiveView socket
     57   - A plug `redirect_if_user_is_authenticated` that redirects to a default path in case the user is authenticated - useful for a registration page that should only be shown to unauthenticated users
     58 - **Always let the user know in which router scopes, `live_session`, and pipeline you are placing the route, AND SAY WHY**
     59 - `phx.gen.auth` assigns the `current_scope` assign - it **does not assign a `current_user` assign**
     60 - Always pass the assign `current_scope` to context modules as first argument. When performing queries, use `current_scope.user` to filter the query results
     61 - To derive/access `current_user` in templates, **always use the `@current_scope.user`**, never use **`@current_user`** in templates or LiveViews
     62 - **Never** duplicate `live_session` names. A `live_session :current_user` can only be defined __once__ in the router, so all routes for the `live_session :current_user`  must be grouped in a single block
     63 - Anytime you hit `current_scope` errors or the logged in session isn't displaying the right content, **always double check the router and ensure you are using the correct plug and `live_session` as described below**
     64 
     65 ### Routes that require authentication
     66 
     67 LiveViews that require login should **always be placed inside the __existing__ `live_session :require_authenticated_user` block**:
     68 
     69     scope "/", AppWeb do
     70       pipe_through [:browser, :require_authenticated_user]
     71 
     72       live_session :require_authenticated_user,
     73         on_mount: [{DailyTrackerWeb.UserAuth, :require_authenticated}] do
     74         # phx.gen.auth generated routes
     75         live "/users/settings", UserLive.Settings, :edit
     76         live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
     77         # our own routes that require logged in user
     78         live "/", MyLiveThatRequiresAuth, :index
     79       end
     80     end
     81 
     82 Controller routes must be placed in a scope that sets the `:require_authenticated_user` plug:
     83 
     84     scope "/", AppWeb do
     85       pipe_through [:browser, :require_authenticated_user]
     86 
     87       get "/", MyControllerThatRequiresAuth, :index
     88     end
     89 
     90 ### Routes that work with or without authentication
     91 
     92 LiveViews that can work with or without authentication, **always use the __existing__ `:current_user` scope**, ie:
     93 
     94     scope "/", MyAppWeb do
     95       pipe_through [:browser]
     96 
     97       live_session :current_user,
     98         on_mount: [{DailyTrackerWeb.UserAuth, :mount_current_scope}] do
     99         # our own routes that work with or without authentication
    100         live "/", PublicLive
    101       end
    102     end
    103 
    104 Controllers automatically have the `current_scope` available if they use the `:browser` pipeline.
    105 
    106 <!-- phoenix-gen-auth-end -->
    107 
    108 <!-- usage-rules-start -->
    109 
    110 <!-- phoenix:elixir-start -->
    111 ## Elixir guidelines
    112 
    113 - Elixir lists **do not support index based access via the access syntax**
    114 
    115   **Never do this (invalid)**:
    116 
    117       i = 0
    118       mylist = ["blue", "green"]
    119       mylist[i]
    120 
    121   Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
    122 
    123       i = 0
    124       mylist = ["blue", "green"]
    125       Enum.at(mylist, i)
    126 
    127 - Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
    128   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:
    129 
    130       # INVALID: we are rebinding inside the `if` and the result never gets assigned
    131       if connected?(socket) do
    132         socket = assign(socket, :val, val)
    133       end
    134 
    135       # VALID: we rebind the result of the `if` to a new variable
    136       socket =
    137         if connected?(socket) do
    138           assign(socket, :val, val)
    139         end
    140 
    141 - **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
    142 - **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
    143 - 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)
    144 - Don't use `String.to_atom/1` on user input (memory leak risk)
    145 - Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
    146 - Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
    147 - Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
    148 
    149 ## Mix guidelines
    150 
    151 - Read the docs and options before using tasks (by using `mix help task_name`)
    152 - To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
    153 - `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
    154 
    155 ## Test guidelines
    156 
    157 - **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
    158 - **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
    159   - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
    160 
    161       ref = Process.monitor(pid)
    162       assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
    163 
    164    - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
    165 <!-- phoenix:elixir-end -->
    166 
    167 <!-- phoenix:phoenix-start -->
    168 ## Phoenix guidelines
    169 
    170 - Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
    171 
    172 - You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
    173 
    174       scope "/admin", AppWeb.Admin do
    175         pipe_through :browser
    176 
    177         live "/users", UserLive, :index
    178       end
    179 
    180   the UserLive route would point to the `AppWeb.Admin.UserLive` module
    181 
    182 - `Phoenix.View` no longer is needed or included with Phoenix, don't use it
    183 <!-- phoenix:phoenix-end -->
    184 
    185 <!-- phoenix:ecto-start -->
    186 ## Ecto Guidelines
    187 
    188 - **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
    189 - Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
    190 - `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
    191 - `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
    192 - You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
    193 - Fields which are set programmatically, 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
    194 - **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
    195 <!-- phoenix:ecto-end -->
    196 
    197 <!-- phoenix:html-start -->
    198 ## Phoenix HTML guidelines
    199 
    200 - Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
    201 - **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
    202 - 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]`
    203 - **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">`)
    204 - 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)
    205 
    206 - 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.
    207 
    208   **Never do this (invalid)**:
    209 
    210       <%= if condition do %>
    211         ...
    212       <% else if other_condition %>
    213         ...
    214       <% end %>
    215 
    216   Instead **always** do this:
    217 
    218       <%= cond do %>
    219         <% condition -> %>
    220           ...
    221         <% condition2 -> %>
    222           ...
    223         <% true -> %>
    224           ...
    225       <% end %>
    226 
    227 - HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
    228 
    229       <code phx-no-curly-interpolation>
    230         let obj = {key: "val"}
    231       </code>
    232 
    233   Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
    234 
    235 - HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
    236 
    237       <a class={[
    238         "px-2 text-white",
    239         @some_flag && "py-5",
    240         if(@other_condition, do: "border-red-500", else: "border-blue-100"),
    241         ...
    242       ]}>Text</a>
    243 
    244   and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
    245 
    246   and **never** do this, since it's invalid (note the missing `[` and `]`):
    247 
    248       <a class={
    249         "px-2 text-white",
    250         @some_flag && "py-5"
    251       }> ...
    252       => Raises compile syntax error on invalid HEEx attr syntax
    253 
    254 - **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
    255 - HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
    256 - HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
    257 
    258   **Always** do this:
    259 
    260       <div id={@id}>
    261         {@my_assign}
    262         <%= if @some_block_condition do %>
    263           {@another_assign}
    264         <% end %>
    265       </div>
    266 
    267   and **Never** do this – the program will terminate with a syntax error:
    268 
    269       <%!-- THIS IS INVALID NEVER EVER DO THIS --%>
    270       <div id="<%= @invalid_interpolation %>">
    271         {if @invalid_block_construct do}
    272         {end}
    273       </div>
    274 <!-- phoenix:html-end -->
    275 
    276 <!-- phoenix:liveview-start -->
    277 ## Phoenix LiveView guidelines
    278 
    279 - **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and  `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
    280 - **Avoid LiveComponent's** unless you have a strong, specific need for them
    281 - LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
    282 
    283 ### LiveView streams
    284 
    285 - **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
    286   - basic append of N items - `stream(socket, :messages, [new_msg])`
    287   - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
    288   - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
    289   - deleting items - `stream_delete(socket, :messages, msg)`
    290 
    291 - When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
    292 
    293       <div id="messages" phx-update="stream">
    294         <div :for={{id, msg} <- @streams.messages} id={id}>
    295           {msg.text}
    296         </div>
    297       </div>
    298 
    299 - LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
    300 
    301       def handle_event("filter", %{"filter" => filter}, socket) do
    302         # re-fetch the messages based on the filter
    303         messages = list_messages(filter)
    304 
    305         {:noreply,
    306          socket
    307          |> assign(:messages_empty?, messages == [])
    308          # reset the stream with the new messages
    309          |> stream(:messages, messages, reset: true)}
    310       end
    311 
    312 - LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
    313 
    314       <div id="tasks" phx-update="stream">
    315         <div class="hidden only:block">No tasks yet</div>
    316         <div :for={{id, task} <- @streams.tasks} id={id}>
    317           {task.name}
    318         </div>
    319       </div>
    320 
    321   The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
    322 
    323 - When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
    324   along with the updated assign:
    325 
    326       def handle_event("edit_message", %{"message_id" => message_id}, socket) do
    327         message = Chat.get_message!(message_id)
    328         edit_form = to_form(Chat.change_message(message, %{content: message.content}))
    329 
    330         # re-insert message so @editing_message_id toggle logic takes effect for that stream item
    331         {:noreply,
    332          socket
    333          |> stream_insert(:messages, message)
    334          |> assign(:editing_message_id, String.to_integer(message_id))
    335          |> assign(:edit_form, edit_form)}
    336       end
    337 
    338   And in the template:
    339 
    340       <div id="messages" phx-update="stream">
    341         <div :for={{id, message} <- @streams.messages} id={id} class="flex group">
    342           {message.username}
    343           <%= if @editing_message_id == message.id do %>
    344             <%!-- Edit mode --%>
    345             <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
    346               ...
    347             </.form>
    348           <% end %>
    349         </div>
    350       </div>
    351 
    352 - **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
    353 
    354 ### LiveView JavaScript interop
    355 
    356 - Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
    357 - **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
    358 
    359 LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
    360 and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
    361 
    362 #### Inline colocated js hooks
    363 
    364 **Never** write raw embedded `<script>` tags in heex as they are incompatible with LiveView.
    365 Instead, **always use a colocated js hook script tag (`:type={Phoenix.LiveView.ColocatedHook}`)
    366 when writing scripts inside the template**:
    367 
    368     <input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
    369     <script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
    370       export default {
    371         mounted() {
    372           this.el.addEventListener("input", e => {
    373             let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
    374             if(match) {
    375               this.el.value = `${match[1]}-${match[2]}-${match[3]}`
    376             }
    377           })
    378         }
    379       }
    380     </script>
    381 
    382 - colocated hooks are automatically integrated into the app.js bundle
    383 - colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
    384 
    385 #### External phx-hook
    386 
    387 External JS hooks (`<div id="myhook" phx-hook="MyHook">`) must be placed in `assets/js/` and passed to the
    388 LiveSocket constructor:
    389 
    390     const MyHook = {
    391       mounted() { ... }
    392     }
    393     let liveSocket = new LiveSocket("/live", Socket, {
    394       hooks: { MyHook }
    395     });
    396 
    397 #### Pushing events between client and server
    398 
    399 Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
    400 **Always** return or rebind the socket on `push_event/3` when pushing events:
    401 
    402     # re-bind socket so we maintain event state to be pushed
    403     socket = push_event(socket, "my_event", %{...})
    404 
    405     # or return the modified socket directly:
    406     def handle_event("some_event", _, socket) do
    407       {:noreply, push_event(socket, "my_event", %{...})}
    408     end
    409 
    410 Pushed events can then be picked up in a JS hook with `this.handleEvent`:
    411 
    412     mounted() {
    413       this.handleEvent("my_event", data => console.log("from server:", data));
    414     }
    415 
    416 Clients can also push an event to the server and receive a reply with `this.pushEvent`:
    417 
    418     mounted() {
    419       this.el.addEventListener("click", e => {
    420         this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
    421       })
    422     }
    423 
    424 Where the server handled it via:
    425 
    426     def handle_event("my_event", %{"one" => 1}, socket) do
    427       {:reply, %{two: 2}, socket}
    428     end
    429 
    430 ### LiveView tests
    431 
    432 - `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
    433 - Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
    434 - Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
    435 - **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
    436 - **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
    437 - Instead of relying on testing text content, which can change, favor testing for the presence of key elements
    438 - Focus on testing outcomes rather than implementation details
    439 - Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
    440 - When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
    441 
    442       html = render(view)
    443       document = LazyHTML.from_fragment(html)
    444       matches = LazyHTML.filter(document, "your-complex-selector")
    445       IO.inspect(matches, label: "Matches")
    446 
    447 ### Form handling
    448 
    449 #### Creating a form from params
    450 
    451 If you want to create a form based on `handle_event` params:
    452 
    453     def handle_event("submitted", params, socket) do
    454       {:noreply, assign(socket, form: to_form(params))}
    455     end
    456 
    457 When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
    458 
    459 You can also specify a name to nest the params:
    460 
    461     def handle_event("submitted", %{"user" => user_params}, socket) do
    462       {:noreply, assign(socket, form: to_form(user_params, as: :user))}
    463     end
    464 
    465 #### Creating a form from changesets
    466 
    467 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:
    468 
    469     defmodule MyApp.Users.User do
    470       use Ecto.Schema
    471       ...
    472     end
    473 
    474 And then you create a changeset that you pass to `to_form`:
    475 
    476     %MyApp.Users.User{}
    477     |> Ecto.Changeset.change()
    478     |> to_form()
    479 
    480 Once the form is submitted, the params will be available under `%{"user" => user_params}`.
    481 
    482 In the template, the form form assign can be passed to the `<.form>` function component:
    483 
    484     <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
    485       <.input field={@form[:field]} type="text" />
    486     </.form>
    487 
    488 Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
    489 
    490 #### Avoiding form errors
    491 
    492 **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**:
    493 
    494     <%!-- ALWAYS do this (valid) --%>
    495     <.form for={@form} id="my-form">
    496       <.input field={@form[:field]} type="text" />
    497     </.form>
    498 
    499 And **never** do this:
    500 
    501     <%!-- NEVER do this (invalid) --%>
    502     <.form for={@changeset} id="my-form">
    503       <.input field={@changeset[:field]} type="text" />
    504     </.form>
    505 
    506 - You are FORBIDDEN from accessing the changeset in the template as it will cause errors
    507 - **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
    508 <!-- phoenix:liveview-end -->
    509 
    510 <!-- usage-rules-end -->