Search code examples
elixirphoenix-frameworkecto

How to handle associations and nested forms in Phoenix framework?


What is the way to handle associations and nested forms in Phoenix framework? How would one create a form with nested attributes? How would one handle it in the controller and model?


Solution

  • There is a simple example of handling 1-1 situation.

    Imagine we have a Car and an Engine models and obviously a Car has_one Engine. So there's code for the car model

    defmodule MyApp.Car do
      use MyApp.Web, :model
    
      schema "cars" do
        field :name, :string            
    
        has_one :engine, MyApp.Engine
    
        timestamps
      end
    
      def changeset(model, params \\ :empty) do
        model
        |> cast(params, ~w(name), ~w())
        |> validate_length(:name, min: 5, message: "No way it's that short")    
      end
    
    end
    

    and the engine model

    defmodule MyApp.Engine do
      use MyApp.Web, :model
    
      schema "engines" do
        field :type, :string            
    
        belongs_to :car, MyApp.Car
    
        timestamps
      end
    
      def changeset(model, params \\ :empty) do
        model
        |> cast(params, ~w(type), ~w())
        |> validate_length(:type, max: 10, message: "No way it's that long")    
      end
    
    end
    

    Simple template for the form ->

    <%= form_for @changeset, cars_path(@conn, :create), fn c -> %>
    
      <%= text_input c, :name %>
    
      <%= inputs_for c, :engine, fn e -> %>
    
        <%= text_input e, :type %>
    
      <% end %>  
    
      <button name="button" type="submit">Create</button>
    
    <% end %>
    

    and the controller ->

    defmodule MyApp.CarController do
      use MyApp.Web, :controller
      alias MyApp.Car
      alias MyApp.Engine
    
      plug :scrub_params, "car" when action in [:create]
    
      def new(conn, _params) do    
        changeset = Car.changeset(%Car{engine: %Engine{}})    
        render conn, "new.html", changeset: changeset
      end
    
      def create(conn, %{"car" => car_params}) do    
        engine_changeset = Engine.changeset(%Engine{}, car_params["engine"])
        car_changeset = Car.changeset(%Car{engine: engine_changeset}, car_params)
        if car_changeset.valid? do
          Repo.transaction fn ->
            car = Repo.insert!(car_changeset)
            engine = Ecto.Model.build(car, :engine)
            Repo.insert!(engine)
          end
          redirect conn, to: main_page_path(conn, :index)
        else
          render conn, "new.html", changeset: car_changeset
        end
      end    
    
    end
    

    and an interesting blog post on the subject that can clarify some things as well -> here