Dynamic Polymorphic Forms in Rails 7
It was time to move my Meal Planning app from AirTable to a custom application. AirTable’s pricing for the size of the database wasn’t awful, but I’d moved past the free account limits. And there were some features I wanted to add that were hard with the basic AirTable features.
It’s been a while, so let’s write a Rails app!
I exported CSVs from AirTable, ran bundle && rails new and got to test-driving. Rails 7 and Resource scaffolding just worked as I expected. Recipes and Restaurants came together very quickly.
As soon as I tried to model a calendar of days, where days could have one or more restaurants and one or more recipes (a key feature of the current solution), things got complex.
I ran into three problems:
- Rails and forms with
has_manyassociations - But the association is polymorphic
- But the form really needs to be dynamic in order to add/remove things from the association
Complex Forms
I had my Recipe and Restaurant models and full controllers. Now I wanted to add a Day model that has a date as well has has_many :recipes as a starting point.
Rails has gotten pretty mature when it comes to the fields_for helper that basically gives you a nested form that includes your association. The Complex Forms section of the Action View Form Helpers Rails Guide talks about People with more than one address. This is great for explaining how form_with and fields_for work together.
To keep things simple, I only tried to add and edit one Recipe on a Day. It was working great. Now it was time to make the association polymorphic.
Polymorphic has-and-belongs-to-many
I needed to be able to add one recipe or one restaurant. I tried several different modelings of the associations to get a polymorphic has_many to work and couldn’t get all the tests passing. Then I found this StackOverflow post that was. I needed a separate join table with some has_many through’s’.
Of course! I needed something that looked more like:
Day <-> Meal (Course <-> [Recipe | Restaurant])
Which got me to these models:
class Day < ApplicationRecord
validates :date, uniqueness: true
validates :date, presence: true
has_many :meals, dependent: :destroy
end
class Meal < ApplicationRecord
belongs_to :day
belongs_to :course, polymorphic: true
end
class Recipe < ApplicationRecord
validates :name, presence: true
has_many :meals, as: :course
has_many :days, through: :meals
end
class Restaurant < ApplicationRecord
validates :name, presence: true
has_many :meals, as: :course
has_many :days, through: :meals
end
(Note: I’m not thrilled with the name of course, but it’s close enough.)
Now with the form, I could put one Recipe select and one Restaurant select and be able to have one or the other. There were some bugs, sure. But I was creating, updating, and saving Days. Progress!
Next question - how do I get the form properly dynamic, adding as many recipes and restaurants as I wanted?
Dynamic Complex Polymorphic Forms
There’s a nice caveat at the Rails Guide for forms:
Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an “Add new address” button. Rails does not provide any built-in support for this.
Whoops. I’m on my own.
To recap, I knew that what I wanted was for a Day form to start with no Meals, but then be able to add or delete many Recipes and Restaurants and submit that form at the end to create or update the Day.
Time to learn Turbo and Hotwire.
I found two great pages that helped immensely. First, was Alexis Chávez’s guide to Turbo & Hotwire. The concepts were clear - it’s a bit like old RJS, but more formal thanks to how Controllers have evolved. But how do I structure the form?
Back to StackOverflow, and a nicely detailed question Rails 7 Dynamic Nested Forms with hotwire/turbo frames. It models cocktails with ingredients and looked a lot like what I needed.
I had some patterns! Time for some code.
DaysController gets new methods
First, a couple of new routes:
resources :days do
get :new_meal, on: :collection, path: "new_meal/:course_type"
delete :destroy_meal, on: :collection, path: "destroy_meal/(:id)"
end
Then in the Day form I added two Turbo links that call the #new_meal method: one to add a Recipe and one to add a Restaurant.
.meals
...
= link_to new_meal_days_path("Recipe"), data: { turbo_method: :get } do
%i.fa-regular.fa-square-plus
= "Add Recipe"
= link_to new_meal_days_path("Restaurant"), data: { turbo_method: :get } do
%i.fa-regular.fa-square-plus
= "Add Restaurant"
Then in DaysController:
def new_meal
helpers.fields model: Day.new do |f|
f.fields_for :meals,
Meal.new,
child_index:
Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) do |ff|
render turbo_stream:
turbo_stream.append("meals",
partial: "meal_fields",
locals: {f: ff, klass: params[:course_type] })
end
end
end
Copying from the “Cocktails” post, this method:
- Creates a form helper that will build the tags for a
Dayand then oneMeal - Passes the
fields_forform helper (ff) to the view, along with thecourse_typethat came in from theGET
And then the meals_fields view…
- div_id = "meal_#{f.index}"
%div{id: div_id}
= f.hidden_field :id
= f.hidden_field :course_type, value: course_type
.course_edits
= f.collection_select :course_id,
course_type.constantize.order(:name),
:id,
:name,
prompt: "Select a #{course_type}..."
= link_to destroy_meal_days_path(id: f.object.id, div_id: div_id),
data: { turbo_method: :delete } do
%i.fa-regular.fa-square-minus
This builds a div with a unique ID (from the timestamp in the controller method) that renders:
- Hidden fields for the
Mealidandcourse_type - A select with that type’s list (yay,
String#constantize!) - A link to destroy this section of the form, if the user so chooses
And that link routes back to DaysController#destory_meal
def destroy_meal
Meal.find(params[:id]).destroy! if params[:id]
render turbo_stream: turbo_stream.remove(params[:div_id])
end
Which will…
- Find and destroy the
Mealif this is aDay#editand so anidis provided; but ignores it if not - Removes this
Meal’sdivinside theDayform
It’s Alive!

Phew! Thanks, Internet!