Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 200
NoDisplayName Avatar asked Aug 17 '15 15:08

NoDisplayName


2 Answers

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

like image 93
NoDisplayName Avatar answered Nov 03 '22 13:11

NoDisplayName


Ran into the same problem with a has_many relationship. Unfortunately, a Car cannot have many Engines, so I'd take the same example in this blogpost, of a TodoList, with many TodoItems

TodoList model:

defmodule MyApp.TodoList do
  use MyApp.Web, :model

  schema "todo_lists" do
    field :title, :string            

    has_many :todo_items, MyApp.TodoItem

    timestamps
  end
 
  def changeset(model, params \\ :{}) do
    model
    |> cast(params, [:title])
    |> cast_assoc(:todo_items)
  end 
end

TodoItem model:

defmodule MyApp.TodoItem do
  use MyApp.Web, :model

  schema "todo_items" do
    field :body, :string

    belongs_to :todo_list, MyApp.TodoList

    timestamps
  end

  def changeset(model, params \\ :{}) do
    model
    |> cast(params, [:body])
  end
end

Here is the form creation a TodoList. To keep things simple let's just add one item for now.

<%= form_for @changeset, todo_lists_path(@conn, :create), fn f -> %>    
  <%= text_input f, :title %>  
  <%= inputs_for f, :todo_items, fn i -> %> 
    <%= text_input i, :body %> 
  <% end %>   
  <button name="button" type="submit">Create</button> 
<% end %>

This is how TodoListController would look like. The create method was the trickiest to get right. I had to dig into Ecto Tests to find a way to make this work. Link

defmodule MyApp.TodoListController do
  use MyApp.Web, :controller

  alias MyApp.TodoList
  alias MyApp.TodoItem

  def new(conn, _params) do
    todo_item = TodoItem.changeset(%TodoItem{})
    changeset = TodoList.changeset(%TodoList{todo_items: [todo_item]})

    render conn, "new.html", changeset: changeset
  end

  def create(conn, %{"todo_list" => todo_list_params}) do
    todo_item_changeset =
      TodoItem.changeset(%TodoItem{}, todo_item["todo_items"]["0"])
    changeset =
      TodoList.changeset(%TodoList{}, %{title: todo_list_params["title"]})
      |> Ecto.Changeset.put_assoc(:todo_items, [todo_item_changeset])

    case Repo.insert(changeset) do
      {:ok, company} ->
        conn
        |> put_flash(:info, "TodoList created!")
        |> redirect(to: page_path(conn, :index))
      {:error, changeset} ->
        conn
        |> render "new.html", changeset: changeset
    end
  end
end
like image 31
Pranav Avatar answered Nov 03 '22 14:11

Pranav