Reputation: 2843
let me first start by saying this could also be a modeling problem and I am open to model suggestions.
Use Case: I have a form and I need to allow a user to select a checkbox on the category of their post. If there is no category that fits their post checking the other category will show a text field for the user to add a custom category. This should would for creating and updating nested modules
DB Modeling
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.string :name, null: false
t.timestamps null: false
end
reversible do |dir|
dir.up {
Category.create(name: 'Hats')
Category.create(name: 'Shirts')
Category.create(name: 'Pants')
Category.create(name: 'Shoes')
Category.create(name: 'Other')
}
end
create_table :categorizations, id: false do |t|
t.belongs_to :post, index: true, null: false
t.belongs_to :category, index: true, null: false
t.string :value
end
end
end
App Models
class Post < ActiveRecord::Base
has_many :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categories
end
class Category < ActiveRecord::Base
has_many :posts
end
Controller:
def update
if @post.update(post_params)
flash.now[:success] = 'success'
else
flash.now[:alert] = @post.errors.full_messages.to_sentence
end
render :edit
end
private
def set_post
@post = Post.find(params[:id])
(Category.all - @post.categories).each do |category|
@post.categorizations.build(category: category)
end
@post.categorizations.to_a.sort_by! {|x| x.category.id }
end
def post_params
params.require(:post).permit(:name, :description,
categorizations_attributes: [ :category_id, :value, :_destroy],
)
end
View:
= f.fields_for :categorizations do |ff|
= ff.check_box :_destroy, { checked: ff.object.persisted? }, '0', '1'
= ff.label :_destroy, ff.object.category.name
= ff.hidden_field :category_id
= ff.text_field :value if ff.object.category.other?
However with the above solution i continue to run in to duplicate record errors when saving. Not sure why this is happening? Is there a better way to do this?
Upvotes: 6
Views: 1329
Reputation: 134
Instead of having the user select the "Other" Category and then storing the text field somewhere else, you should create a new Category instance instead. You are on the right track with the accepts_nested_attributes_for
.
The next step would be:
# app/controllers/posts_controller.rb
def new
@post = Post.new
@post.build_category
end
private
# don't forget strong parameters!
def post_params
params.require(:post).permit(
...
category_attributes: [:name]
...
)
end
Views (using simple_form and nested_form gems)
# app/views/new.html.haml
= f.simple_nested_form_for @job do |f|
= f.simple_fields_for :category do |g|
= g.input :name
You can also do it cleaner using Form Objects instead.
Edit: If you need to separate concerns of the Other categories from the Original categories, you can use OO inheritance to do so. The Rails Way of doing this is Single Table Inheritance.
# app/models/category.rb
class Category < ActiveRecord::Base
end
# app/models/other_category.rb
class OtherCategory < Category
end
# app/models/original_category.rb
class OriginalCategory < Category
end
Upvotes: 0
Reputation: 136
I would prefer something like this:
Models
post.rb
class Post < ActiveRecord::Base
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
accepts_nested_attributes_for :categories
end
category.rb
class Category < ActiveRecord::Base
has_many :categorizations
has_many :posts, through: :categorizations
end
Controller
...
def update
if @post.update(post_params)
flash.now[:success] = 'success'
else
flash.now[:alert] = @post.errors.full_messages.to_sentence
end
render :edit
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:name, :description, category_ids: [])
end
...
Views
I always preferer plain .erb
so,
with help of simple_form.
<%= simple_form_for(@post) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<%= f.input :content -%>
...
</div>
<div class="form-inputs">
<%= f.association :categories, as: :check_boxes -%>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
You can have checked/unchecked states and destroy easily and cleanly by this way. In addition, you can add
<%= f.simple_fields_for :category do |category_fields| %>
<%= category_fields.input :name -%>
<% end %>
to get nested fields for associations, but don't forget to add related params to strong_params
when you do this.
...
def post_params
params.require(:post).permit(:name, :description, category_attributes: [:name])
end
....
Upvotes: 4
Reputation: 3578
Don't store the other in your model, nor it's name! If you're using form_for
for your posts
, simply add an unrelated field.
ex: f.text_field :other_name
to text_field_tag :other_name
Manually add your Other
option to the dropdown collection.
You can add JS to hide and display a hidden text field if other is selected.
In your posts_controller do:
def create
...
if params[:other_name]
post.categories.create(name: param[:other_name])
end
...
end
Upvotes: 2