Nicemaker

Nicemaker

I have a Product schema with a many_to_many relation for Tags.
On the Edit Product Page I want to create a simple form that looks like this:

Name
[ Product A ]

Tags
[ Tag A ] [Tag B] [Tag C] [Tag D]

The bold Tags are the Tags that the Product has assigned, the other ones are not assigned yet. In my case its more user friendly to show all of the available tags because the number is quite limited.

The behaviour of the Tags should be pretty much like checkboxes, when you click them they should either be selected or unselected.

I want to have one “Save” button that becomes active ones the name or the tags have changed.

I tried to merge the approach of two very good tutorials to make this happen:

  1. Making a CheckboxGroup Input · The Phoenix Files
  2. Sorting and Deleting many-to-many assocs with Ecto and LiveView · The Phoenix Files

Both don’t really cover my UI case.
I also read this solution just now, which is what I want to achieve:

But as the author stated himself, also, I’m not too fond of view-related code in the Ecto shemas.

Here is my naive newbie approach, which nearly works:

Create Schemas

defmodule Shopex.Products do

  schema "products" do
    field :name, :string
    many_to_many :tags, Shopex.Products.Tag,
      join_through: "product_tag_relations", 
      on_replace: :delete
    ...
  end

  def changeset(product, attrs) do
    product
    |> cast(attrs, [:name,:archived_at])
    |> validate_required([:name])
    |> cast_assoc( :tags )
  end
end

defmodule Shopex.Products.Tag do

  schema "product_tags" do
    field :name, :string
    ...
  end 

  ... 
end

Create Live View Controller Edit Product

The get_product function preloads the assigned Tags.
But I am also getting a full list of all available Tags and assigning it to the socket:

  @impl true
  def handle_params(%{"id" => id}, _, socket) do
    product = Products.get_product!(id)
    tags = Products.list_product_tags()
    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
     |> assign(:product,product)
     |> assign(:tags, tags)
     |> assign_form(Products.change_product(product))}
  end

Create Simple Form View

<.simple_form
        for={@form}
        id="product-form"
        phx-change="validate"
        phx-submit="save"
      >
       <.input field={@form[:name]} type="text" label="Name" />
       <.tags options={@tags} field={@form[:tags]}/>

        <:actions>
          <.button type="submit" phx-disable-with="Saving...">Save</.button>
        </:actions>

      </.simple_form>

Create Tags Component

I am creating in the first line a helper list to figure out if the checkbox further down should be selected or not.

That works on the first load. But as you will see further down it will fail as soon as I change one of the checkboxes.

def tags(assigns) do
    selected_ids = Enum.map(assigns.field.value, &(&1.id) )        
    ~H"""
      <div>
        <.label>Tags</.label>
        <div class="flex gap-1">
          
          <div class="flex item-center" :for={tag<- @options}>
            <label
              for={"#{@field.name}-#{tag.id}"} class="font-medium text-gray-700">
              <input
                type="checkbox"
                id={"#{tag.id}"}
                name={"#{@field.name}[]"}
                value={tag.id}
                checked={tag.id in selected_ids}
                class="mr-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 transition duration-150 ease-in-out"
              />
              <%= tag.name  %>
            </label>
          </div>
        </div>
      </div>
    """
  end
 

Extend Validation Handler

def handle_event("validate", %{"product" => product_params}, socket) do
   
    tags = socket.assigns.tags
    |> Enum.filter(&(&1.id in product_params["tags"]))
    |> Enum.map fn x ->Map.from_struct(x) end
    params = Map.replace!(product_params, "tags", tags)

    changeset =
      socket.assigns.product
      |> Products.change_product( params )
      |> Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}
  end

And Break it

So far so good, the change_product actually works.

But alas!
As soon as I change a checkbox it breaks

[error] GenServer #PID<0.9720.0> terminating
** (KeyError) key :id not found in: #Ecto.Changeset<action: :insert, changes: %{name: "Sensitive"}, errors: [], data: #Shopex.Products.Tag<>, valid?: true>
    (shopex 0.1.0) lib/shopex_web/components/tags.ex:14:

Because on change the assigns.field.value is not longer the list of preloaded Tags but a list of changesets.

%Phoenix.HTML.FormField{  
  ...,
  value: [
    #Ecto.Changeset<
      action: :insert,
      changes: %{name: "Sensitive"},
      errors: [],
      data: #Shopex.Products.Tag<>,
      valid?: true
    >,
    #Ecto.Changeset<action: :update, changes: %{}, errors: [],
     data: #Shopex.Products.Tag<>, valid?: true>
  ]

Is this even a good approach at all? Any ideas how to fix that last little bit?

Showing Posts 1 to 3

felix-starman

felix-starman

I’m on mobile so please excuse me if this is I missed something in the code examples.

It seems like you care more about the UX then capturing the change attempts.

It might be easier to use CSS to show or hide a submit button.

You could set it as:

name={"#{@field.name}[tag.id or something unique to the tag but not the relation]"}
value="true"
class={[if already_persisted?(...), do: "persisted", else: "not-persisted"}

and have a hidden checkbox for each tag that defaults to false so when you get a change event with everything not just the checked items.

Then a CSS rule for:

button[type=submit] ~ input.tag.not-persisted[checked]

You could also do this with data attributes, but then you’d need to use the :not function in CSS or something like that

I hope this gets it across but I think since this is mostly a display concern that you should be able to surface the tag info in a way that you can do it almost entirely in CSS and save yourself some headache

Nicemaker

Nicemaker OP

Thanks for the quick reply @felix-starman I just noticed that my sentence describing the “Save” button was quite bad. It should have been:

In the end I would love to have only one “Save” button for the whole Edit Product Page, that is only enabled if the tags or the name field have changed.
For this reason, I thought to keep the tag selection and name field in one simple_ form. But I haven’t got to this part because I struggle with the rerendering the tags ones the tag field has changed.

So I am till at the phase of validation, the save button stuff comes after.

Nicemaker

Nicemaker OP

I finally decided also to use the solution via “put_assoc” as described in this Post.

I found a way to work without the described virtual fields.
Please check the other Post for the solution.

— 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
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews