7 Patterns to Refactor Fat ActiveRecord Models - here is a great article about different refactoring approaches using PORO. Under the 3rd caption there is a Form Object pattern, which I really liked and already implemented in one of the projects. There is only an example using one nested resource, but I would like to implement this pattern for multiple nested resources. Maybe someone here had already dealt with this? I don't necessarily need any code examples, just the basic idea would be fine.
Update
Consider this example. I have two models.
class Company
has_many :users
accepts_nested_attributes_for :users
end
class User
belongs_to :company
end
In case of one nested user for company using Form Object Pattern I would write the following:
<%= form_for #company_form do |f| %>
<%= f.text_field :name %>
<%= f.text_field :user_name %>
<%= f.submit %>
<% end %>
Form Object
class CompanyForm
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_accessor :company, :user
def user
#user ||= company.users.build
end
def company
#company ||= Company.new
end
def submit(params={})
company.name = params[:name]
user.name = params[:user_name]
persist!
end
private
def persist!
company.save!
user.save!
end
end
But what if I have a form, where a company with multiple users can be created. The usual approach is to write it like this, using nested_form:
<%= nested_form_for #company do |f| %>
<%= f.text_field :name %>
<%= fields_for :users, do |user_form| %>
<%= user.form.text_field :name %>
<% end %>
<%= f.link_to_add "Add a user", :users %>
<%= f.submit %>
<% end %>
What I am asking is how do I implement that Form Object Pattern in this case?
the rails fields_for helper checks for a method in this format: #{association_name}_attributes=
so, if you add this method to CompanyForm:
def users_attributes=(users_attributes)
# manipulate attributes as desired...
#company.users_attributes= users_attributes
end
def users
company.users
end
the fields_for generators will generate the nested users fields for a CompanyForm as if it were a Company. the above could be rewritten as a delegation since nothing is happening in the methods:
delegate :users, :users_attributes=, :to => :company, :prefix => false, :allow_nil => false
Related
I'm trying to make an invoicing app. The form to create an invoice should include a collection of check boxes so the user can choose which lessons to invoice, but I'm getting this error: undefined method 'collection_check_boxes'.
Here are the models involved:
class Lesson < ActiveRecord::Base
attr_accessible :lesson_time, :lesson_date, :invoice_id
belongs_to :invoice
end
class Invoice < ActiveRecord::Base
attr_accessible :amount, :due_date
has_many :lessons
end
And the view:
<%= form_for(#invoice) do |f| %>
<fieldset>
<%= f.label :lessons %>
<%= f.collection_check_boxes(:lessons, Lesson.all, :id, :lesson_date) %>
<%= f.submit %>
</fieldset>
<% end %>
collection_check_boxes is not a method of form_builder. Either put:
<%= collection_check_boxes(:lessons, Lesson.all, :id, :lesson_date) %>
This will generate html which won't associate with your model (you won't be able to use MyModel.new(params[my_model]) and expect to get proper response. You would either have to manually call my_model.lessons = params[:lessons] or you can pass a html name parameter to conform your check box name to rails convention).
Or, if you are using formtastic as you tagged it, you can use this:
<%= f.input :lessons, :as => :check_boxes, :collection => Lesson.all %>
I suspect that since you tagged your post ruby-on-rails-3, you might be trying to use a rails 4 method inside a rails 3 project.
http://makandracards.com/makandra/32147-rails-4-introduced-collection_check_boxes
You'll likely need to use good old check_box_tag instead.
I have two models. User have_many articles. How can I make one view for this models with simple_form gem? I can't find this part in wiki and readme :(
For example, something like "simple_form_for [ user, articles ] do |f|"
You can use the simple_fields_for method to create sub-forms for your main model's associations.
<%= simple_form_for #user do |f| %>
<%= f.input :name %>
<%= simple_fields_for :articles do |article_form| %>
<%= article_form.input :title %>
<% end %>
<% end %>
Also, don't forget to tell your model to accept nested attributes, or the record will be invalid.
class User < ActiveRecord::Base
has_many :articles
attr_accessible :articles_attributes
accepts_nested_attributes_for :articles
end
I have a Sezzion model:
attr_accessible :description
has_many :session_instructors, :dependent => :destroy
has_many :instructors, :through => :session_instructors
accepts_nested_attributes_for :session_instructors
accepts_nested_attributes_for :instructors
Instructor model:
attr_accessible :bio
has_many :sezzions, :through => :session_instructors
has_many :session_instructors, :dependent => :destroy
SessionInstructor model:
attr_accessible :instructor_id, :sezzion_id
belongs_to :sezzion
belongs_to :instructor
Lastly, User model:
has_many :sezzions
has_many :instructors
I'm trying to create a form for Sezzion with nested form for SessionInstructor which has multiple select option for Instructors.
How can I do the following:
nested form for SessionInstructor
use multiple select option to get all the selected Instructor's instructor_id
hidden field to pass in the created/updated session_id with each select instructor
I have the following code as of now:
<%= form_for(#sezzion) do |f| %>
<%= f.label :description %>
<%= f.text_area :description %>
<%= f.label :instructors %>
<%= fields_for :session_instructors do |f| %>
<select multiple>
<% current_user.instructors.each do |instructor| %>
<option><%= instructor.name %></option>
<% end %>
</select>
<% end %>
<%= f.submit %>
<% end %>
Thank you so much!
This is something that seems ridiculously hard in Rails.
I think something like this might work:
<%= f.fields_for :session_instructors do |si| %>
<%= si.collection_select :instructor_ids, current_user.instructors, :id, :name, multiple: true>
<% end %>
This should create a form element which will set sezzion[session_instructors_attributes][instructor_ids].
Although I'm not sure if that's actually what you want. I've never tried this using a multi select. If it doesn't work, you could also try getting rid of the fields_for and just using f.collection_select. If you're willing to use a checkbox, I can show you how to do that for sure.
I hope that helps.
Edit:
Here's how I would usually do it with a check_box:
<%= f.fields_for :session_instructors do |si| %>
<%= si.hidden_field "instructor_ids[]" %>
<% current_user.instructors.each do |i| %>
<%= si.check_box "instructor_ids[]", i.id, i.sezzions.include?(#sezzion), id: "instructor_ids_#{i.id}" %>
<%= label_tag "instructor_ids_#{i.id}", i.name %>
<% end %>
<% end%>
There are a couple "gotchas!" with this method. When editing a model, if you deselect all checkboxes then it won't send the parameter at all. That's why the hidden_field is necessary. Also, you need to make sure each form element has a unique id field. Otherwise only the last entry is sent. That's why I manually set the value myself.
I copy pasted and then edited. Hopefully I got the syntax close enough where you can get it to work.
FINAL EDIT:
Per Sayanee's comment below, the answer was a bit simpler than I thought:
<%= f.collection_select :instructor_ids, current_user.instructors, :id, :name, {}, {:multiple => true} %>
#Sayanee, can you post how your instructors, sezzions table look like. Also for note, you can not get instructor_ids from Instructor object, hence you are getting "undefined method" error. With the current association that you shared, you can get instructor_ids from a Sezzion object. So you need to loop through current_user.sezzions in stead of current_user.instructors.
This is a way to implement fields_for nested form with html multiple_select in case of a has_many :through association. Solved it by doing something like this. The form view:
<%= form_for(#sezzion) do |f| %>
...
<%= fields_for :session_instructors do |g| %>
<%= g.label :instructor, "Instructees List (Ctrl+Click to select multiple)" %>:
<%= g.select(:instructor_id, Instructor.all.collect { |m| [m.name, m.id] }, {}, { :multiple => true, :size => 5 }) %>
<%= g.hidden_field :your_chosen_variable_id, value: your_chosen.id %>
<% end %>
...
<%= f.submit %>
<% end %>
Note:Since the #sezzion would not be saved at the time of generating the form you cannot pass that id (#sezzion.id) in place of your_chosen.id through the form. You could handle that save in the controller.
Make sure that your controller Initializes the Variables while generating the form: Your def new could look something like this:
def new
#sezzion = Sezzion.new
#sezzion.session_instructor.build
#sezzion.instructors.build
end
Now the create controller has to be able to accept the strong params required for the multiple select, so the sezzion_params method may look something like this:
def sezzion_params
params.require(:sezzion).permit(:description, :any_other_fields,
:session_instructors_attributes =>
[:instructor_id => [], :your_chosen_id => Integer])
end
In the create function, the first session_instructor variable is linked to the #sezzion instance variable through our new function. The other session_instructors in our multiple select must be built after the Sezzion instance is saved, if you want to pass in the created #sezzion.id with each select instructor. .
def create
#sezzion = Sezzion.new(sezzion_params)
#startcount=1 #The first element of the array passed back from the form with multiple select is blank
#sezzion.session_instructors.each do |m|
m.instructor_id = sezzion_params["session_instructors_attributes"]["0"]["instructor_id"][#startcount]
m.your_chosen_variable_id = your_chosen.id
#startcount +=1
end
if #sezzion.save
sezzion_params["session_instructors_attributes"]["0"]["instructor_id"].drop(#startcount).each do |m|
#sezzion.session_instructors.build(:instructor_id => sezzion_params["session_instructors_attributes"]["0"]["instructor_id"][#startcount],
:your_chosen_variable_id => your_chosen.id).save
#startcount += 1
end
flash[:success] = "Sezzion created!"
redirect_to root_url
else
flash[:danger] = "There were errors in your submission"
redirect_to new_sezzion_path
end
end
Given a User who can possibly be an Artist:
class User < ActiveRecord::Base
has_one :artist
end
I've got a User & Artist nested form (using Formtastic gem):
<h1>Artist registration</h1>
<% #user.build_artist unless #user.artist %>
<%= semantic_form_for #user, :url => create_artist_path do |f| %>
<%= f.inputs :username %>
<%= f.semantic_fields_for :artist do |a| %>
<%= a.input :bio %>
<% end %>
<%= f.buttons do %>
<%= f.commit_button 'Register as Artist' %>
<% end %>
<% end %>
The problem is the :artist fields are not rendered.
I've also tried f.inputs :for => :artist do |a|.
For some reason, using #user.build_artist does not display the artist's fields in the form. If I try #user.artist = Artist.new I get an error, because it tries to save the Artist and validation fails.
How should I initialize the Artist model so I get the benefit of formtastic generators in a nested form? (Note that #user here is not a :new_record?)
Did you remember to set accepts_nested_attributes_for :artist in user.rb?
I have pulled out all my hair. No more left... :(
I am using Spree 0.3.4, within an extension I need to register some retailers up. so I direct them to a retailers form which has many custom fields which belong to a retailer model...
So I am trying to validate/submit all the fields from one form like so
myextension/app/views/user_registrations/new.html.erb
<%= form_for (:user, :url => registration_path(#user, :type => "retailer) do |f| %>
<%= f.fields_for :retailer do |r| %>
<%= r.text_field :name %>
<% end %>
<%= f.text_field :email %>
<% end %>
etc etc
class Retailer < ActiveRecord::Base
belongs_to :user
validates :name,
:presence => true
end
class User < ActiveRecord::Base
has_one :retailer
accepts_nested_attributes_for :retailer
attr_accessible :retailer_attributes
# theres a whole lot more spree and devise stuff here. not sure worth mentioning
end
I have also added the abilities in the cancan ability.rb
The problem is the retailer feilds never get validated and the data is never inserted into the database...
I created a blank app, and tried this process from scratch with some plain old scaffolding and it works fine.
any ideas??
In your application helper, do something like this(assuming your have Ruby 1.9.* for the tap functionality, otherwise checkout rails returning here):
def setup_user(user)
user.tap do |u|
u.build_retailer if u.retailer.nil?
end
end
then in your view change it to this:
<%= form_for (setup_user(#user), :url => registration_path(#user, :type => "retailer) do |f| %>
See if that works.