Sandwiches in the Expanded Field, or, “Happiness is...sharing a sandwich.”
Sando Club is about friendship. Members eat sandwiches and collect stamps together in exchange for fun and prizes. Sandwich Club is about documentation. Members record their best sandwich meals and comments in a Google Sheet. Sando Club is ephemeral; Sandwich Club is a database.
You can access Sandwich Club’s Goggle Sheet via a link on their website. Anyone with the link can record sandwiches and comments. I saved this sheet as a comma-separated values (CSV) file and used Ruby’s CSV class methods to parse the file and seed the database for a Rails application. I wanted to preserve both the functions of the sheet and its ethos, i.e. anyone can access the database and add valid entries by code of honor, no user sign-up or login required.
In the app you can view a list of all the sandwiches, see individual sandwich entries, view each member and a list of all the sandwiches she has eaten, add a new sandwich, and leave comments for each sandwich.
A particular challenge of adding a new sandwich is the ability to create a new eater and assign her to that sandwich. To do this I employed nested attributes.
class Sandwich < ApplicationRecord
has_many :eater_sandwiches
has_many :eaters, through: :eater_sandwiches
has_many :comments
validates :ingredients, :eaters, :tasting_notes, :date, presence: true
accepts_nested_attributes_for :eaters
accepts_nested_attributes_for :comments, reject_if: :blank_comment?
private
def blank_comment?(attributes)
attributes[:text].blank? || attributes[:eater_id].blank?
end
end
According to Rails documentation:
“Nested attributes allow you to save attributes on associated records through the parent. By default nested attribute updating is turned off and you can enable it using the #accepts_nested_attributes_for class method. When you enable nested attributes an attribute writer is defined on the model. The attribute writer is named after the association.”
In my example this means that a new method has been added to the model:
eater_attributes=(attributes)
And in the sandwiches controller I must whitelist the new associated params:
def sandwich_params
params.require(:sandwich).permit(:ingredients, :date, :location, :price, :tasting_notes, eater_ids: [], eater_attributes: [:id, :name], comment_attributes: [:text, :sandwich_id, :eater_id])
end
My new sandwich form looks like this:
form_for @sandwich do |f|
f.label :eater_ids, "Whom ate this?"
f.select(:eater_ids, @eaters.map {|e| [e.name, e.id] }, {prompt: "Select one or many"}, {multiple: true, size: 8})
f.fields_for :eater do |ff|
ff.label :name, "Add an eater not on this list"
ff.text_field :name
ff.hidden_field :id
end
f.submit "Add Sandwich"
end
And for the sandwich controller’s create action:
@eater = Eater.find_or_create_by(name: params[:sandwich][:eater][:name])
In these snippets you can see that I also have nested attributes for comments. On an individual sandwich page you can create a new comment to associate with that sandwich and at the same time create a new user for that comment.
The form for updating a sandwich will have a nested field for adding a new comment which in turn has a nested field for adding an eater.
form_for @sandwich do |f|
f.fields_for :comment do |ff|
ff.text_area :text, size: "150x2"
ff.fields_for :eater do |fff|
fff.label :name, "Your Name"
fff.text_field :name
fff.hidden_field :id
end
end
f.submit "Add Comment"
end
I added to the comment model:
accepts_nested_attributes_for :eater
and whitelisted the attributes in the comment controller:
def comment_params
params.require(:comment).permit(:text, :sandwich_id, :eater_id, eater_attributes: [:id, :name])
end
And in the sandwich controller’s update action I shall need:
@comment = Comment.new(text: params[:sandwich][:comment][:text], sandwich_id: @sandwich.id)
@eater = Eater.find_or_create_by(name: params[:sandwich][:comment][:eater][:name])
Nested attributes are not so terrible and you many never have to use them! The Sixth Annual Sandwich Club Summit will take place in September in Wassaic, NY.