Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the proper way of handling nested forms / ecto changesets in Phoenix?

I am writing a simple CRUD app in Phoenix, where admins, upon creating a new organisation are allowed to provision it with an initial staff member account.

Effectively the relationship between Organisations and Users is many to many.

I came up with the following:

  1. User schema:

    defmodule MyApp.User do
    use MyApp.Web, :model
    
    schema "users" do
      field :name, :string
      field :email, :string
      field :password, :string, virtual: true
      field :password_hash, :string
    end
    
    def changeset(...) # validate email, password confirmation etc.
    
  2. Organisation schema:

    defmodule MyApp.Org do
      use MyApp.Web, :model
    
      schema "orgs" do
        field :official_name, :string
        field :common_name, :string
    
        has_many :org_staff_users, MyApp.OrgStaffUser
        has_many :users, through: [:org_staff_users, :user]
     end
    
     def changeset(model, params \\ :empty) do
      model
      |> cast(params, ~w(official_name common_name), [])
     end
    
     def provisioning_changeset(model, params \\ :empty) do
       model
       |> changeset(params)
       |> cast_assoc(:org_staff_users, required: true)
     end
    
  3. Junction table org_staff_users and the corresponding Ecto Schema with user_id and org_id

  4. Controller with the following new action:

     def new(conn, _params) do
       data = %Org{org_staff_users: [%User{}]}
       changeset = Org.provisioning_changeset(data)
       render(conn, "new.html", changeset: changeset)
     end
    
  5. Template with the following excerpt:

     <%= form_for @changeset, @action, fn f -> %>
          <%= if @changeset.action do %>
            <div class="alert alert-danger">
              <p>Oops, something went wrong! Please check the errors below:</p>
              <ul>
                <%= for {attr, message} <- f.errors do %>
                  <li><%= humanize(attr) %> <%= message %></li>
                <% end %>
              </ul>
            </div>
          <% end %>
    
        <%= text_input f, :official_name, class: "form-control" %>
        <%= text_input f, :common_name, class: "form-control" %>
    
        <%= inputs_for f, :org_staff_users, fn i -> %>
            <%= text_input f, :email, class: "form-control" %>
            <%= text_input f, :password, class: "form-control" %>
            <%= text_input f, :password_confirmation, class: "form-control" %>
        <% end %>
    
        <%= submit "Submit", class: "btn btn-primary" %>
    <% end %>
    

So far so good, the form displays nicely.

The problem is, I don't really understand what should be the canonical way of building the changeset I'm about to insert on create, while being able to pass it again to the view upon validation errors.

It is unclear whether I should use one changeset (and how?) or explicitly three changesets per each entity (User, Org and the junction table).

How do I validate the changes for such combined form, given that each model / schema has its own specific validations defined?

The params I receieve upon submitting the form are all within %{"org" => ...} map, including the ones that are in fact related to a user. How should I create the form properly?

I have read the recently updated http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/ but I remain confused regardless.

FWIW, I am on Phoenix 1.0.4, Phoenix Ecto 2.0 and Phoenix HTML 2.3.0.

Any tips would be greatly appreciated.

like image 214
Ragnar Lodbrok Avatar asked Dec 23 '15 20:12

Ragnar Lodbrok


1 Answers

Right now, you don't have any other option besides doing everything in a transaction. You are going to create an organization inside the transaction, which its own changeset, and if it works you create each staff. Something like this:

if organization_changeset.valid? and Enum.all?(staff_changesets, & &1.valid?) do
  Repo.transaction fn ->
    Repo.insert!(organization_changeset)
    Enum.each staff_changesets, &Repo.insert!/1)
  end
end

Notice I am doing valid? checks on the changesets which is non-ideal because it doesn't consider constraints. If there are constraints in the changesets though, you need to use Repo.insert (without bang !).

Keep in mind this will be much easier on Ecto 2.0. On Ecto master, we already support belongs_to via changesets, which means you would be able to do it explicitly by generating both intermediate and end associations:

<%= inputs_for f, :org_staff_users, fn org_staff -> %>
  <%= inputs_for org_staff, :user, fn user -> %>
    # Your user form here
  <% end %>    
<% end %>

However, we will also support many_to_many which will make it altogether straight-forward.

like image 159
José Valim Avatar answered Sep 28 '22 16:09

José Valim