Dynamic nested forms

Hello,
I wanted to ask you if you could point me in the right direction on how to do something that I’ve been banging my head against for a couple of days now. I have a table user which links with a has_many relationship to a table sibling that contains two fields name and surname. If I were to create a form that lets you add siblings and for each new sibling add the name and surname, how would you suggest I go on about doing it? What I mean is how can I add a “add” button to the user form that lets me add a new sibling every time i press it?
as of now I made the form multipart and added the inputs_for f, :siblings p nested form which lets me add a sibling per user.
I then followed an article from AlchemistCamp to make the form dynamic and added the add and remove buttton with relative js. problem is the article i followed is imprinted on adding only one field to fill an array structure rather than a separate model, so I was wondering if anybody had ever done that and could point me in the right direction.

Thank you very much

I thnk you can achieve that via javascript. Check how phoenix creates inputs and add a event click listener to a button and append child to these to increase numbers each time. Also add |> cast_assoc(:siblings)in your changeset after cast.
For example:

<input class="form-control" id="user_siblings_1_name" name="user[siblings][1][name]" type="text">
<input class="form-control" id="user_siblings_1_surname" name="user[siblings][1][surname]" type="text">
1 Like

Check this post. It might help.

Best regards,

Joaquín Alcerro

2 Likes

Thanks for your help :slight_smile: I solved it using the Formex library :slight_smile: it made things quite easy actually

3 Likes

Can you show your solution please?

Hi, yeah I’ll try to remember cause it has been a while :slight_smile:

add this to your model

use Formex.Ecto.Schema

add this to your schema

formex_collection_child()

then you create a form type like so

defmodule Assistant.Web.ServiceType do
  use Formex.Type
  use Formex.Ecto.Type
  use Formex.Ecto.ChangesetValidator
  use Arc.Ecto.Schema
  import Ecto.Changeset, only: [cast: 3]

  def build_form(form) do
    form
    |> add(:name, :text_input, label: "Service Name", validation: [:required])
    |> add(:description, :textarea, label: "Service Description",
      phoenix_opts:
        [
          rows: 5
        ],
      validation: [:required])
    |> add(:image, :file_input, label: "Image",
      validation: [:required])

in the controller you use the create form module you created

 |> create_form(%Detail{}, detail_params)
    |> insert_form_data
    |> case do
      {:ok, detail} ->
        conn
        |> put_flash(:info, "Clinic's Details created successfully.")
        |> Map.put(detail, :id, 1)
        |> redirect(to: clinic_detail_path(conn, :index))
      {:error, form} ->
        render(conn, "new.html", form: form)
      end

then in the template you use the helper methods provided

<%= formex_form_for @form, @action, [multipart: true], fn f -> %>

  <%= if @form.submitted? do %>Oops, something went wrong!<% end %>

  <%= formex_row f, :name %>
  <%= formex_row f, :motto %>
  <%= formex_row f, :logo %>
  <%= formex_row f, :brief %>
  <%= formex_row f, :opening_hours %>
  <%= formex_row f, :contact %>
  <%= formex_row f, :image %>
  


  <% collection = fn collection -> %>
    <div class="form-horizontal">
      <%= formex_collection_items collection %>
      <%= formex_collection_add collection, "Add Service" %>
    </div>
  <% end %>

  <% collection_item = fn subform -> %>
    <%= formex_collection_remove {:safe, "&times;"}, "Are you sure you want to remove?" %>
    <%= formex_row subform, :name %>
    <%= formex_row subform, :description %>
    <%= formex_row subform, :image %>
  <% end %>

  <%= formex_collection f, :services, [template: Formex.Template.BootstrapHorizontal], collection, collection_item %>

  <%= formex_row f, :save %>
  
<% end %>

for more in depth and precise information the library page is really good formex github

I hope I have been able to help, as I mentioned it has been a while. Any questions just ask :slight_smile:
Good luck!

If you don’t want to use Formex here’s an example of nested form. Assuming User has_one Profile:

defmodule Database.Schema.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias Database.Schema.{
    User,
    Profile,
    Post
  }

  schema "users" do
    field :email, :string
    field :password, :string
    has_one :profile, Profile
    has_many :posts, Post

   timestamps()
  end

  # @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password])
    |> validate_required([:email, :password])
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
  end

  def changeset_assoc(%User{} = user, attrs) do
    user
    |> changeset(attrs)
    |> cast_assoc(:profile, required: true)
  end
end
defmodule Database.Schema.Profile do
  use Ecto.Schema
  import Ecto.Changeset

  alias Database.Schema.User

  schema "profiles" do
    field :address, :string
    field :name, :string
    field :phone, :string
    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(profile, attrs) do
    profile
    |> cast(attrs, [:name, :phone, :address])
    |> validate_required([:name, :phone, :address])
  end
end

Here’s the user repository for creating changesets:

defmodule Database.Repo.User do
  import Ecto.Query, warn: false

  alias Database.Repo
  alias Database.Schema.User

  def create_assoc(attrs \\ %{}) do
    %User{}
    |> User.changeset_assoc(attrs)
    |> Repo.insert()
  end

  def change_assoc(%User{} = user) do
    User.changeset_assoc(user, %{})
  end
end

Inside your controller you can do something like this:

defmodule Frontend.User.AuthController do
  use Frontend, :controller

  def register(conn, params) do
    case params do
      %{"user" => user_params}
        -> case Database.Repo.User.create_assoc(user_params) do
            {:ok, user} ->
              conn
              |> put_session(:user_id, user.id)
              |> put_flash(:info, "User created successfully.")
              |> redirect(to: home_path(conn, :index))
            {:error, %Ecto.Changeset{} = changeset} ->
              render(conn, "register.html", changeset: changeset)
          end
      _ -> render(conn, "register.html", changeset: Database.Repo.User.change_assoc(%Database.Schema.User{}))
    end
  end
end

and inside your template you can use the nested form:

<section class="service-layout1 bg-accent s-space-custom2">
  <div class="container">
    <div class="section-title-dark">
      <h1>Register</h1>
    </div>
    <div class="row">
      <div class="col-lg-4 col-md-4 col-sm-6 col-xs-6 col-mb-12 item-mb">
        <%= form_for @changeset, auth_path(@conn, :register), fn f -> %>
          <%= inputs_for f, :profile, fn p -> %>
            <div class="form-group">
              <%= label p, :name, class: "control-label" %>
              <%= text_input p, :name, class: "form-control" %>
              <%= error_tag p, :name %>
            </div>
          <% end %>

          <div class="form-group">
            <%= label f, :email, class: "control-label" %>
            <%= text_input f, :email, class: "form-control" %>
            <%= error_tag f, :email %>
          </div>

          <div class="form-group">
            <%= label f, :password, class: "control-label" %>
            <%= text_input f, :password, class: "form-control" %>
            <%= error_tag f, :password %>
          </div>

          <%= inputs_for f, :profile, fn p -> %>
            <div class="form-group">
              <%= label p, :phone, class: "control-label" %>
              <%= text_input p, :phone, class: "form-control" %>
              <%= error_tag p, :phone %>
            </div>

            <div class="form-group">
              <%= label p, :address, class: "control-label" %>
              <%= text_input p, :address, class: "form-control" %>
              <%= error_tag p, :address %>
            </div>
          <% end %>

          <div class="form-group">
            <%= submit "Submit", class: "btn btn-primary" %>
          </div>
        <% end %>
      </div>
    </div>
  </div>
</section>

Hope will help :slight_smile:

2 Likes

If somebody is looking for a solution that uses only Javascript, I have just created this library that should do this work.

The code I have there is just a start after facing this problem again in my new project. So it’s just about adding/removing group of fields. Feel free to contribute if you have any ideas :smile:

2 Likes