Search code examples
elixirphoenix-frameworkecto

Show related data from two schemas in the same eex template


I am trying to show some related data in a form. I have a Students schema

schema "students" do
  field :firstname, :string
  field :lastname, :string
  field :birthday, Ecto.Date
  field :joined_on, Ecto.Date
  field :gender, :string
  timestamps()
end

Each student can have a mother and a father, these I have designated them as Parents and created a separate schema and separate table. Each parent record can be linked with max 1 student (odd, but works for my use case).

schema "parents" do
  field :relationship, :string
  field :fullname, :string
  field :qualification, :string
  belongs_to :student, Myapp.Student, foreign_key :student_id
  timestamps()
end

I am trying to create a form where I can create new Students and Parents at the same time. So there will be fields for Student and fields for Mother and Father in the same form. I've tried multiple ways to get this Parents data in the eex template but I'm failing at it.

Here is what I'm using in the new method of the Student controller:

def new (conn, _params) do
  changeset = Student.changeset(%Student{})
  render(conn, "new.html", [changeset: changeset])
end

I think there's something wrong here because I'm not able to get the parents fields in the eex form when I use the Html helpers. I want to be able to create new students, parents at the same time. Also it would be great if I can edit them using the same eex template. Thanks.


Solution

  • You need to use has_many and cast_assoc in Student model:

    defmodule Myapp.Student do
      use Myapp.Web, :model
    
      schema "students" do
        field :firstname, :string
        field :lastname, :string
        field :birthday, Ecto.Date
        field :joined_on, Ecto.Date
        field :gender, :string
        has_many :parents, Myapp.Parent
    
        timestamps()
      end
    
      @doc """
      Builds a changeset based on the `struct` and `params`.
      """
      def changeset(struct, params \\ %{}) do
        struct
        |> cast(params, [:firstname, :lastname, :birthday, :joined_on, :gender])
        |> cast_assoc(:parents)
        |> validate_required([:firstname, :lastname, :birthday, :joined_on, :gender])
      end
    end
    

    In web/templates/student/form.html.eex use inputs_for:

    <%= inputs_for f, :parents, fn pf -> %>
      <h3>Parent form</h3>
    
      <div class="form-group">
        <%= label pf, :relationship, class: "control-label" %>
        <%= text_input pf, :relationship, class: "form-control" %>
        <%= error_tag pf, :relationship %>
      </div>
    
      <div class="form-group">
        <%= label pf, :fullname, class: "control-label" %>
        <%= text_input pf, :fullname, class: "form-control" %>
        <%= error_tag pf, :fullname %>
      </div>
    
      <div class="form-group">
        <%= label pf, :qualification, class: "control-label" %>
        <%= text_input pf, :qualification, class: "form-control" %>
        <%= error_tag pf, :qualification %>
      </div>
    <% end %>
    

    You can build two parents in Student controller action new:

    alias Myapp.Parent
    
    def new(conn, _params) do
      changeset = Student.changeset(%Student{parents: [%Parent{}, %Parent{}]})
      render(conn, "new.html", changeset: changeset)
    end
    

    Also need to preload :parents in StudentController for edit and update actions:

    student = Repo.get!(Student, id) |> Repo.preload(:parents)
    

    The code - https://github.com/shhavel/stackoverflow-questions-39683691