ravecat

ravecat

Select field for multiple association

How can I implement a form with m2m field (select shown in the screenshot)?

I would like to emphasize that this is not a dynamic form, an example of which can be found in the repository GitHub - LostKobrakai/one-to-many-form: Example repo for blogpost · GitHub, but it’s a field.

I also know about the possibility of linking via put_assoc in the documentation, which fits this case (Adding tag to a post Ecto.Changeset — Ecto v3.14.0), but it doesn’t mention anything about form implementation.

I also know about live_select, phx-multi-select, etc, It isn’t about component implementation

It’s about matchig schema to form

  schema "projects" do
    field :name, :string
    field :description, :string
    has_many :locales, Locales, on_replace: :delete
    belongs_to :team, Team
    belongs_to :base_language, Language

    many_to_many :languages, Language,
      join_through: Locale,
      on_replace: :delete

    timestamps type: :utc_datetime
  end

and make multi selection for locales field. I don’t understand, what do I have to work with? Language labels, id languages?

Implemented this via virtual fields, but it seems like I’m doing it wrong

  1. There is a Project that is linked to Languages through an intermediate table runa/lib/domains/projects/project.ex at 6cced23a560c0780391c8268563d587bda347290 · ravecat/runa · GitHub
  2. Passing virtual field to form fields runa/lib/application/live/project/form.ex at 6cced23a560c0780391c8268563d587bda347290 · ravecat/runa · GitHub and processed during validation in the same component.
  3. in the changset for the form I already bind by real fields runa/lib/domains/projects/project.ex at 6cced23a560c0780391c8268563d587bda347290 · ravecat/runa · GitHub

it seems that this approach complicates and contains the issues and could be done more simply (edited)

First Post!

Zurga

Zurga

You pass can pass the id of the language as the second item of the option tuple. Then you use put_assoc in the changeset function.

Last Post!

ravecat

ravecat

Thank you for your response

Thats works for for standart select form, it’s first advice which I hear when I show a field like this. Could you explain what field should process with put_assoc? And where is it should process (form, changeset)?

we can skip complex combobox mechanics and just use multiselect from core components

form represent changeset, where I prepare data for multiselect

defmodule RunaWeb.Live.Project.Form do
  @moduledoc """
  Form responsible for creating and updating projects.
  """
  use RunaWeb, :live_component

  alias Runa.Languages
  alias Runa.Projects
  alias Runa.Projects.Project

  import RunaWeb.Components.Form
  import RunaWeb.Components.Input

  @impl true
  def mount(socket) do
    {:ok, {languages, _}} = Languages.index()

    {:ok, assign(socket, languages: Enum.map(languages, &{&1.title, &1.id}))}
  end

  @impl true
  def update(%{data: %Project{} = data} = assigns, socket) do
    data =
      data
      |> maybe_preload(:base_language)
      |> maybe_preload(:locales)

    attrs = %{
      "locales" =>
        if(is_nil(data.id),
          do: [],
          else: Enum.map(data.locales, & &1.id)
        )
    }

    socket =
      socket
      |> assign(assigns)
      |> assign(data: data)
      |> assign(
        action: if(is_nil(data.id), do: :new, else: :edit),
        form: to_form(Project.changeset(data, attrs))
      )

    {:ok, socket}
  end

  slot(:actions, doc: "the slot for form actions, such as a submit button")

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.custom_form
        id={@id}
        for={@form}
        phx-change="validate"
        phx-submit="submit"
        phx-target={@myself}
        aria-label="Project form"
      >
        <.input type="hidden" field={@form[:team_id]} value={@team_id} />
        <.input type="text" aria-label="Project name" field={@form[:name]}>
          <:label>Name</:label>
        </.input>
        <.input
          type="textarea"
          aria-label="Project description"
          field={@form[:description]}
        >
          <:label>Description</:label>
        </.input>
        <.input type="select" field={@form[:base_language_id]} options={@languages}>
          <:label>Base language</:label>
        </.input>
        <.input type="select" field={@form[:locales]} options={@languages} multiple>
          <:label>Languages</:label>
        </.input>

        {render_slot(@actions, @form)}
      </.custom_form>
    </div>
    """
  end

  @impl true
  def handle_event("validate", %{"project" => attrs}, socket) do
    dbg(attrs)

    socket =
      update(socket, :form, fn _, %{data: data} ->
        changeset =
          Project.changeset(data, attrs)

        to_form(changeset, action: :validate)
      end)

    {:noreply, socket}
  end

  @impl true
  def handle_event("submit", %{"project" => attrs}, socket) do
    submit(socket, socket.assigns.action, attrs)
  end

  @impl true
  def handle_event(_, _, socket) do
    {:noreply, socket}
  end

  defp submit(socket, :edit, attrs) do
    socket.assigns.data
    |> Projects.update(attrs)
    |> case do
      {:ok, _} ->
        {:noreply, socket}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp submit(socket, :new, attrs) do
    attrs
    |> Projects.create()
    |> case do
      {:ok, _} ->
        {:noreply, socket}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp put_locales(attrs) do
    locales =
      attrs["locales"]
      |> Enum.map(&%{"language_id" => &1})

    Map.put(attrs, "locales", locales)
  end
end

project changeset

defmodule Runa.Projects.Project do
  @moduledoc """
  Schema for the projects entity.
  """
  use Runa, :schema

  alias Runa.Files.File
  alias Runa.Keys.Key
  alias Runa.Languages.Language
  alias Runa.Languages.Locale
  alias Runa.Teams.Team

  schema "projects" do
    field :name, :string
    field :description, :string
    has_many :files, File
    has_many :keys, Key
    has_many :locales, Locale, on_replace: :delete
    belongs_to :team, Team
    belongs_to :base_language, Language

    many_to_many :languages, Language, join_through: Locale, on_replace: :delete

    timestamps type: :utc_datetime
  end

  def changeset(struct, attrs \\ %{}) do
    struct
    |> cast(attrs, [:name, :description, :team_id, :base_language_id])
    |> validate_required([:name, :team_id, :base_language_id])
    |> foreign_key_constraint(:team_id)
    |> foreign_key_constraint(:base_language_id)
    |> cast_assoc(:locales)
  end
end

project relates with languages through locales schema

defmodule Runa.Languages.Locale do
  @moduledoc """
  Schema for the locale entity.
  """
  use Runa, :schema

  alias Runa.Languages.Language
  alias Runa.Projects.Project

  schema "locales" do
    belongs_to :project, Project
    belongs_to :language, Language

    timestamps type: :utc_datetime
  end

  @doc false
  def changeset(locale, attrs) do
    locale
    |> cast(attrs, [:project_id, :language_id])
    |> validate_required([:language_id])
    |> foreign_key_constraint(:project_id)
    |> unique_constraint([:project_id, :language_id])
  end
end

After choose another select options I’ve got error, because I work with language id, but projects expect locale as map

Where Next?

Popular in Questions Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40165 209
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New

We're in Beta

About us Mission Statement