DEV Community

Noel Worden
Noel Worden

Posted on

1

Assigning Two Fields from One Dropdown Selection with Elixir

This week I was tasked with having to assign two fields based on the select of a single dropdown. There are undoubtedly more elegant ways to solve this problem, with multiple dropdowns, or a dropdown that is populated based on the selection of of a previous dropdown. But, because of the stack we are currently working with, and the bounds I had to work within, my task was very explicit: assign two data points based on the selection from one dropdown.

Based on the route I took to accomplish this task, it ended up being more of an exercise in changeset manipulation than getting clever with the dropdown itself. The exact data I had to work with needs to stay private, so I have come up with a loose example with different content, but that strives for the same end result.

Let's say there's a form that allows a user to choose his or her favorite movie from a dropdown of options. But, for analytical purposes, you also wanted to be able to record the data point of what his or her favorite movie genre is (like I said, this is a pretty loose example).

There would be a Survey schema, and at the end of this example that schema will have two belongs_to associations, one to :movie, and another to :genre. So, if the user selected Caddyshack as the movie, the genre field would be set to comedy, and if the user selected The Shinning, the genre field would be set to horror.

A list of tuples that include each movie record's name and id, to be used as the data for the movie select dropdown, would look something like this:

[
{"Alien", "alien"},
{"American History X", "amercican_history_x"},
{"Blazing Saddles", "blazing_saddles"},
{"Caddyshack", "caddyshack"},
{"Nightmare on Elm Street", "nightmare_on_elm_street"},
{"No Country for Old Men", "no_country_for_old_men"},
{"Princess Bride", "princess_bride"},
{"The Departed", "the_departed"},
{"The Shining", "the_shining"}
]

A simple schema module for this one question survey would look something like this, if we don't worry about the genre yet:

defmodule MyApp.Schemas.Survey do
use MyApp.Schema
import Ecto.Changeset
alias MyApp.Schemas.Movie
schema "survey" do
belongs_to :movie, Movie, type: :string
timestamps()
end
def changeset(survey, attrs) do
survey
|> cast(attrs, :movie_id)
|> validate_required(:movie_id)
end
end

Pretty straightforward, user is shown a collection on movie titles, and will select his or her favorite. Now, to add in the genre. I found this could be done by adding a private function that assigns genre_id based on movie_id.

defp add_genre(changeset) do
movie_id = get_field(changeset, :movie_id)
cond do
movie_id in [
"caddyshack",
"blazing_saddles",
"princess_bride"
] ->
change(changeset, %{genre_id: "comedy"})
movie_id in [
"nightmare_on_elm_street",
"the_shining",
"alien"
] ->
change(changeset, %{genre_id: "horror"})
movie_id in [
"no_country_for_old_men",
"the_departed",
"amercican_history_x"
] ->
change(changeset, %{genre_id: "drama"})
end
end

Let me break it down a bit. A variable movie_id is set, and then a cond statement is entered, and based on what the content of the movie_id variable is, the genre_id field is set accordingly using the change/2 function provided by Ecto. Since there is a dropdown with predictable options, the conditional options can be hard coded in this way.

There is one catch, it's standard practice to pass an empty changeset when sending a user to a /new route in the controller. With that being the case, that empty changeset would enter this private function, and error out, because there is no conditional for movie_id: nil. The error shown in the logs would be:

no cond clause evaluated to a truthy value

Again, because the data being evaluated is bound by what is provided in the dropdown, and the dropdown selection in this case would be set as required in the form html, it can be assumed that the only time an empty movie_id field reaches this logic is on the initial /new page render. Knowing this, a sort of catch-all can be added to the end of the conditional:

true ->
changeset

With that at the bottom of the cond statement it will now return the empty changeset when rendering the /new form. The entire function would now look like this:

defp add_genre(changeset) do
movie_id = get_field(changeset, :movie_id)
cond do
movie_id in [
"caddyshack",
"blazing_saddles",
"princess_bride"
] ->
change(changeset, %{genre_id: "comedy"})
movie_id in [
"nightmare_on_elm_street",
"the_shining",
"alien"
] ->
change(changeset, %{genre_id: "horror"})
movie_id in [
"no_country_for_old_men",
"the_departed",
"amercican_history_x"
] ->
change(changeset, %{genre_id: "drama"})
true ->
changset
end
end

Now, this private function can be inserted into the changeset, and the validate_required check can be updated to include genre_id:

def changeset(survey, attrs) do
survey
|> cast(attrs, :movie_id)
|> add_genre()
|> validate_required(:movie_id, :genre_id)
end

And the Survey schema can be updated to include the genre_id association, so the entire schema would be updated to look like this:

defmodule MyApp.Schemas.Survey do
use MyApp.Schema
import Ecto.Changeset
alias MyApp.Schemas.{Genre, Movie}
schema "survey" do
belongs_to :genre, Genre, type: :string
belongs_to :movie, Movie, type: :string
timestamps()
end
def changeset(survey, attrs) do
survey
|> cast(attrs, :movie_id)
|> add_genre()
|> validate_required(:movie_id, :genre_id)
end
defp add_genre(changeset) do
movie_id = get_field(changeset, :movie_id)
cond do
movie_id in [
"caddyshack",
"blazing_saddles",
"princess_bride"
] ->
change(changeset, %{genre_id: "comedy"})
movie_id in [
"nightmare_on_elm_street",
"the_shining",
"alien"
] ->
change(changeset, %{genre_id: "horror"})
movie_id in [
"no_country_for_old_men",
"the_departed",
"amercican_history_x"
] ->
change(changeset, %{genre_id: "drama"})
true ->
changset
end
end
end

With all that in place, the genre_id field will be automatically populated based on the content of the user-selected movie_id.

The task of needing to assigning two fields from one dropdown is something that could most definitely have more than one solution. I would love to hear anyone else's take on the how to handle something like this.


This post is part of an ongoing This Week I Learned series. I welcome any critique, feedback, or suggestions in the comments.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Implement features, document your code, or refactor your projects.
Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay