ravecat

ravecat

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)

Showing Posts 1 to 2

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.

ravecat

ravecat OP

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

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews