简体   繁体   中英

Rails 4 How to model a form with a collection of checkboxes with other text_field

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?

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
....

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM