I am trying to make it possible for the users of a web application I am building to create lists of objects that they have created.
For example, the user has a list of objects such as groceries which could be anything from apples to oranges to pop-tarts.
I would then like it to be possible for me to return all of the groceries added to the database by the user and create a list by selecting those that are supposed to be on their grocery list.
Preferably this would be in a style such that they could click check boxes for the ones they wanted and then click save to create a new list.
I have looked into belongs_to, has_many relationships and tried creating a list object which has many groceries, but I can not figure out the form part of this strategy. I would appreciate any/all advice. Thanks!
Here is the code I have currently, I originally omitted it because I do not think I am on the right path, but here it is anyway just in case/provide more context:
Grocery Model:
class Item < ApplicationRecord
belongs_to :list, optional: true
end
List Model
class List < ApplicationRecord
has_many :items
end
The List controller
class ListsController < ApplicationController
before_action :authenticate_user!
layout 'backend'
def index
@lists = List.where(user_id: current_user.id)
end
def show
end
def new
@list = List.new
end
def edit
end
def create
@list = List.new(list_params)
@list.user = current_user
if @list.save
redirect_to list_path(@list.id), notice: 'List was successfully created.'
else
redirect_to list_path(@list.id), notice: 'List was not created.'
end
end
def update
respond_to do |format|
if @list.update(list_params)
format.html { redirect_to @list, notice: 'List was successfully updated.' }
format.json { render :show, status: :ok, location: @list }
else
format.html { render :edit }
format.json { render json: @list.errors, status: :unprocessable_entity }
end
end
end
def destroy
@list.destroy
respond_to do |format|
format.html { redirect_to lists_url, notice: 'List was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def list_params
params.require(:list).permit(:name, :items)
end
end
Not sure what to do about form - was trying something along the lines of http://apidock.com/rails/ActionView/Helpers/FormHelper/check_box
I would solve this by implementing a third model which keeps the association between the groceries and the list. And then you can handle it in forms by using :accepts_nested_attributes_for
.
To give an example, here is how I would structure the models:
class List < ApplicationRecord
has_many :list_items, inverse_of: :list
has_many :items, through: :list_items
# This allows ListItems to be created at the same time as the List,
# but will only create it if the :item_id attribute is present
accepts_nested_attributes_for :list_items, reject_if: proc { |attr| attr[:item_id].blank? }
end
class Item < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_items
end
class ListItem < ApplicationRecord
belongs_to :list, inverse_of: :list_items
belongs_to :item
end
With that model structure in place, here is an example of the view for creating a new List.
<h1>New List</h1>
<%= form_for @list do |f| %>
<% @items.each_with_index do |item, i| %>
<%= f.fields_for :list_items, ListItem.new, child_index: i do |list_item_form| %>
<p>
<%= list_item_form.check_box :item_id, {}, item.id, "" %> <%= item.name %>
</p>
<% end %>
<% end %>
<p>
<%= f.submit 'Create List' %>
</p>
<% end %>
To explain what is happening here, @items
is a preloaded variable to has all the Items that can be added to a list. I loop through each Item and I pass it manually to the FormBuilder method fields_for
.
Because I do this manually, I have to specify the :child_index
at the same time, otherwise each checkbox would get the same name attribute (i.e. name="list[list_item_attributes][0][item_id]"
) as the previous item and they would overwrite each others value when being submitted to the server.
And the FormBuilder method check_box
has the following declaration:
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
So in the above form, I replace those default values so that when a check box is checked, it has the value from item.id
and if it is not checked, the value is blank. Combine this with the declaration of accepts_nested_attributes_for
in the List model, where we say that it should be rejected if the :item_id
is blank, and we get the result of only creating ListItems for the checked Items.
The last thing to make this work, is to permit the nested attributes in the controller, like this:
def allowed_params
params.require(:list).permit(list_items_attributes: [:item_id])
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With