Showing form fields conditionally base on radio input value

I want to show input fields based on the value of a the radio button. Here are the two approaches I tried. This is inside a LiveComponent and I’m on LiveView 1.0.2. What is the best way to implement this simple feature (neither of the below work)?

APPROACH 1

When I click on a radio button, it is selected, but none of the following two ifs evaluate to true so the additional fields are never shown.

    <.form for={@form} phx-change="validate" phx-submit="save" phx-target={@myself}>
      <.input
        label="I am an individual"
        type="radio"
        value="INDIVIDUAL"
        field={@form[:entity_type]}
      />
      <.input
        label="I represent a company"
        type="radio"
        value="COMPANY"
        field={@form[:entity_type]}
      />
      <%= if Phoenix.HTML.Form.input_value(@form, :entity_type) == "INDIVIDUAL" do %>
        <.input label="First Name" type="text" field={@form[:first_name]} />
        <.input label="Last Name" type="text" field={@form[:last_name]} />
      <% end %>
      <%= if Phoenix.HTML.Form.input_value(@form, :entity_type) == "COMPANY" do %>
        <.input label="Company Name" type="text" field={@form[:company_name]} />
      <% end %>
    </.form>

Here is the LiveView code:

  def mount(socket) do
    form_attributes = %{
      "first_name" => "",
      "last_name" => "",
      "company_name" => "",
      "entity_type" => ""
    }

    {:ok,
     assign(
      socket,
       :form,
       to_form(form_attributes)
     )
    }
  end

  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

APPROACH 2

After not getting this to work I also tried this approach where I set a new variable to track which entity_type is selected. This resulted in the if condition being executed, but the radio button not being selected and I have to click it again for it to be selected. Here’s a video: Area - Dubz

    <.form for={@form} phx-change="validate" phx-submit="save" phx-target={@myself}>
      <.input
        label="I am an individual"
        type="radio"
        value="INDIVIDUAL"
        field={@form[:entity_type]}
        phx-change="entity_type_changed"
        phx-target={@myself}
      />
      <.input
        label="I represent a company"
        type="radio"
        value="COMPANY"
        field={@form[:entity_type]}
        phx-change="entity_type_changed"
        phx-target={@myself}
      />
      <%= if @entity_type_selected == "INDIVIDUAL" do %>
        <.input label="First Name" type="text" field={@form[:first_name]} />
        <.input label="Last Name" type="text" field={@form[:last_name]} />
      <% end %>
      <%= if @entity_type_selected == "COMPANY" do %>
        <.input label="Company Name" type="text" field={@form[:company_name]} />
      <% end %>
    </.form>
  def mount(socket) do
    form_attributes = %{
      "first_name" => "",
      "last_name" => "",
      "company_name" => "",
      "entity_type" => ""
    }

    {:ok,
     assign(
      socket,
       :form,
       to_form(form_attributes)
     )
     |> assign(:entity_type_selected, "")
    }
  end

  def handle_event("entity_type_changed", %{"entity_type" => entity_type} = params, socket) do
    {:noreply, assign(socket, :entity_type_selected, entity_type)}
  end

When your "validate" event is called, the form sends back the params in the params variable (the second argument to handle_event/3). You have to actually assign the new values back to the socket - currently you are just throwing them away.

{:noreply, assign(socket, :form, to_form(params))}

This is why your first approach doesn’t work. It’s also why your second approach doesn’t work - you are updating the variable, but you’re also throwing away the new params, so when the update comes back from the server it clobbers the radio button (since the server state was never changed).

The form abstraction needs to deal with invalid and partial values and various types of values. Hence it’s not the best place to check if an input is of a certain value. I’d suggest checking Ecto.Changeset.get_field(@form.source, field) instead if the form is based on a changeset.

1 Like

Thank you, I tried that and it only partially worked. The if conditions worked fine but the radio button remained unselected in the UI. Here’s a video: ex - Dubz

LiveComponent

  def handle_event("validate", %{"entity_type" => entity_type} = params, socket) do
    {:noreply,
     assign(socket, :form, to_form(params))
     |> assign(:entity_type_selected, entity_type)
     |> IO.inspect()}
  end

  def handle_event("validate", params, socket) do
    {:noreply, assign(socket, :form, to_form(params))}
  end

Template:

      <.input
        label="I am an individual"
        type="radio"
        value="INDIVIDUAL"
        field={@form[:entity_type]}
      />
      <.input
        label="I represent a company"
        type="radio"
        value="COMPANY"
        field={@form[:entity_type]}
      />
      <%= if @entity_type_selected == "INDIVIDUAL" do %>
        <.input label="First Name" type="text" field={@form[:first_name]} />
        <.input label="Last Name" type="text" field={@form[:last_name]} />
      <% end %>
      <%= if @entity_type_selected == "COMPANY" do %>
        <.input label="Company Name" type="text" field={@form[:company_name]} />
      <% end %>

Here’s also the core_component.ex part which renders the radio button:

  def input(%{type: "radio"} = assigns) do
    ~H"""
    <div class="flex flex-col gap-2">
      <div class="flex gap-2 items-center">
        <input
          type={@type}
          name={@name}
          id={@id}
          value={Phoenix.HTML.Form.normalize_value(@type, @value)}
          class={[
            "block rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
            @errors == [] && "border-zinc-300 focus:border-zinc-400",
            @errors != [] && "border-rose-400 focus:border-rose-400"
          ]}
          {@rest}
        />
        <.label for={@id}>{@label}</.label>
      </div>
      <div>
        <.error :for={msg <- @errors}>{msg}</.error>
      </div>
    </div>
    """
  end

The IO.inspect prints this in the console:

#Phoenix.LiveView.Socket<
  id: "phx-GBnFKa97P70hyQAI",
  endpoint: AmplifyWeb.Endpoint,
  view: AmplifyWeb.PaymentDestinations,
  parent_pid: nil,
  root_pid: #PID<0.917.0>,
  router: AmplifyWeb.Router,
  assigns: %{
    id: "cpd",
    open: true,
    form: %Phoenix.HTML.Form{
      source: %{
        "_target" => ["entity_type"],
        "_unused_first_name" => "",
        "_unused_last_name" => "",
        "entity_type" => "COMPANY",
        "first_name" => "",
        "last_name" => ""
      },
      impl: Phoenix.HTML.FormData.Map,
      id: nil,
      name: nil,
      data: %{},
      action: nil,
      hidden: [],
      params: %{
        "_target" => ["entity_type"],
        "_unused_first_name" => "",
        "_unused_last_name" => "",
        "entity_type" => "COMPANY",
        "first_name" => "",
        "last_name" => ""
      },
      errors: [],
      options: [],
      index: nil
    },
    source: "payment_destinations_list",
    __changed__: %{
      form: %Phoenix.HTML.Form{
        source: %{
          "_target" => ["entity_type"],
          "_unused_company_name" => "",
          "company_name" => "",
          "entity_type" => "INDIVIDUAL"
        },
        impl: Phoenix.HTML.FormData.Map,
        id: nil,
        name: nil,
        data: %{},
        action: nil,
        hidden: [],
        params: %{
          "_target" => ["entity_type"],
          "_unused_company_name" => "",
          "company_name" => "",
          "entity_type" => "INDIVIDUAL"
        },
        errors: [],
        options: [],
        index: nil
      },
      entity_type_selected: true
    },
    flash: %{},
    entity_type_selected: "COMPANY",
    account_id: "acct_01HFFEWHEP5HA4F0GQPVYBBX3X",
    myself: %Phoenix.LiveComponent.CID{cid: 1}
  },
  transport_pid: #PID<0.910.0>,
  ...
>

BTW, I also tried adding this to the radio input but same effect:

      <.input
       id="company_option"
        label="I represent a company"
        type="radio"
        value="COMPANY"
        field={@form[:entity_type]}
        checked={@entity_type_selected == "COMPANY"} # new line added

    />``

You need to handle the checked state in your input component, it’s not actually added to the real <input> element. It would probably be better to compare the value there instead of passing in checked anyway.

Also, radio buttons are not supposed to share the same id, only the name.

You can try this. I am on mobile so I am a bit limited now.

<.input
  label="I am an individual"
  type="radio"
  value="INDIVIDUAL"
  field={@form[:entity_type]}
  checked={@form[:entity_type].value && @form[:entity_type].value == "INDIVIDUAL"}
/>

For conditional field visibility this would work too

<.input :if={@form[:entity_type].value && @form[:entity_type].value == "INDIVIDUAL"} label="First Name" type="text" field={@form[:first_name]} />

Need to update the form in the validate event

def handle_event("validate", params, socket) do
  {:noreply, assign(socket, :form, to_form(params))}
end