I have an existing form that I create from a changeset and that I assign to my socket using <.simple_form>. Inside of the <.simple_form> I added a <select> field in addition to fields based on fields from the form’s data.
This works well and I am able to retrieve the form data and the extra field as separate keys in the params sent to handle_event.
The problem is that I have a phx-change event that validates the form input using the changeset by setting a new form based on that changeset to the socket. When this happens, whatever has been selected in the non-changeset-tracked <select> field is reset to its default value.
Is there a way to persist the selected value for the non-changeset-tracked <select> field so that it does not get wiped out when the form is reset by the validation?
Have you tried persisting the selected value as a socket assign in handle_event?
You’d also want to update the <option ... selected={...}> to use that assign if set to ensure subsequent re-renders are aware of the previously selected option.
To prevent the value from being wiped from the form on validation, I think you need to re-assign the form during the validation. Additionally, I suggest using the input component from the core_components with the option type="select". Here’s an example:
def render(assigns) do
~H"""
<.simple_form :let={f} for={@form} as="my_params" phx-change="validate">
<.input type="select" field={f[:customer_type]} options={[{"display_value1", 1}, {"display_value2", 2}]} />
</simple_form>
"""
end
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{})}
end
def handle_event("validate", %{"my_params" => params}, socket) do
# do your validation here
validated_params = validate(params)
# this is where you want to re-assign the form to the socket. It should include
# the parameters that were passed from the form to the `handle_event` callback
{:noreply, assign(socket, form: to_form(validated_params)}
end
I think one of the key things here is that you can either pass a list of strings to the options attr of the input component, or you can pass a list of tuples, which will set the option’s value and inner HTML.
I’d argue that you should use a source for the form, which manages all the fields the form deals with. If your schema/changeset doesn’t match up it’s imo not an appropriate source. You could map between the formats at a different level (after form handling is done).
Thank you all for your replies. I was able to combine what a lot of you said into a working, and I believe much more elegant, solution.
The reason I didn’t want to create a new non-changeset source is that I wanted to retain the validation of the changeset. So, I added customer_type as a virtual field to the User schema as @greven suggested. Thank you for that suggestion, I didn’t realize that was possible.
Then, @gus 's suggestion of using <.input> and passing a tuple to its options really helped. I had already created a few helper functions to map the display text with the backing data but I didn’t realize that the <.input> form component already has this functionality. So, I was able to clean up my initial code a bit.
Combining <.input options=...> with the virtual field also allowed me to more safely parse responses using String.to_existing_atom() since I used the type Ecto.Enum in my schema for the virtual field.
The final (truncated for clarity) solution follows:
# user.ex
schema "users" do
field :customer_type, Ecto.Enum,
values: [:individual, :family_admin, :family_member],
virtual: true
...
end
I originally had this reply as a draft reply to a specific post and published it that way without thinking. I’m not sure how to remove it as a reply to a specific post and make it just a thread-level reply.