Complex Rails Form Best Practices - ruby-on-rails

I've got a relatively complex form I'm trying to code efficiently. Most online examples of nested forms deal with very clear hierarchical relationships, mine does not.
Below is the data model. The essential job of the form is to create a "Job Entry" record while at the same time creating a new "Entity" record - which is a person. Several relationships come to bear in this form.
A "Job" is already created. The Job has 1-to-many "Questions" which exist before the user hits this form. However, they must fill in "Answers" to the questions. They also choose one of many pre-created "Job Roles".
The question is how to leverage "form_with" and "fields_for" for all these inter-related models.
My assumption is to ditch built-in helpers and just use a form_tag and roll everything together manually. But maybe there is a "correct" way to roll forms that do not necessarily abide by parent-child relationships? In my example, there is no pure top-level object to start with since many child objects already have records, but maybe I am wrong and Entity should be the starting point?
Entity has_many Job_Roles
Entity has_many Job_Entries
Job has many Job_Roles
Job has_many Job_Entries
Job has_many Questions
Question has many Answers
Answers belong_to Entity
Agency has_many Job_Entries
etc...

There is no need to ditch the built-in helper: Rails has thought about that, it's called nested forms.
Here is an example:
<%= form_with model: #job do |f| %>
Job entries:
<ul>
<%= f.fields_for :job_entries do |je_form| %>
<li>
<%= je_form.label :kind %>
<%= je_form.text_field :kind %>
<%= je_form.label :street %>
<%= je_form.text_field :street %>
...
</li>
<% end %>
</ul>
<% end %>
You can nest as many children forms as you'd like using fields_for. Don't forget to use accepts_nested_attributes_for in the parent models.

Nested forms as Mike proposed are a rails-way solution of your problem. It is ok - but for complex forms, with lot of validations, it may not be the best solution). You could consider using a FormObject pattern instead.
FormObject is a simple ruby class that uou can keep it i.e. in Forms folder and use as below:
class JobEntryForm
include ActiveModel::Model
attr_accessor :customer_id, :agency_id, :name, :question_text #you can use atributes from different models
validates :customer_id, presence: true #you can validate yu attributes as you want - your in necessity to use model validation
def initialize(attributes:)
#customer_id = attributes[:customer_id]
#agency_id = attributes[:agency_id]
#name = attributes[:name]
#question_text = attributes[:question_text]
end
#implement whatever you need
end
than in you controller:
#form = JobEntryForm.new
and you your view:
<%= form_for #form do |f| %>
<%= f.label :customer_id, 'Customer' %>:
<%= f.text_field :customer_id %>
...
<%= f.submit %>
<% end %>
And - at the end - in your controller create method:
def create
#form = CreateJobEntry.new.call(attributes: form_params) #service object to keep your controller clean.
end

Related

Ruby on Rails - add fields from a Model on another Model's form

I have two models Contract and Addendum. Contract has_many :addendums and Addendum belongs_to :contract
When a new Contract is created, automatically will create a new Addendum but some aditional elements are needed to create the new Addendum. How can I add a field value, which is an attribute from Addendum but not from Contract, on the Contract's form?
What you're looking for is a nested form, which is pretty common in RoR. For more information on nested and complex forms, there's a section of a Rails Guide for that. I'd recommend checking out all of the Rails Guides, which are incredibly helpful when learning the framework.
For your specific question, first tell your Contract model to accept_nested_attributes_for your Addendum model.
class Contract < ActiveRecord::Base
has_many :addendum
accepts_nested_attributes_for :addendums
end
Next, open up your contract controller, and do two things. One, build an addendum when making a new contract. Two, allow the nested attributes of addendums (assuming you're using rails 4) in your contract_params method.
class ContractController < ApplicationController
def new
#contract = Contract.new
#addendum = #contract.addendums.build
end
protected
def contract_params
params.require(:contact).permit(:field1, :field2, addendums_attributes: [:id, :value, :other_field])
end
end
Last, add the forms_for helper in your contracts form.
<%= form_for #contract do |f| %>
<!-- contract fields -->
Addendums:
<ul>
<%= f.fields_for :addendums do |addendums_form| %>
<li>
<%= addendums_form.label :value %>
<%= addendums_form.text_field :value %>
<!-- Any other addendum attributes -->
</li>
<% end %>
</ul>
<% end %>
With that, you should be all set! Happy coding!

Form has_many in text_area

I have the following situation:
A Order has many Pages. I want to let the User to paste a bunch (20+) URLs (it's a Page attribute) that they might have in a doc file into a text area.
Right now I am not using a Form associated with an Order object, because I fail to see how I can do a nested form of the URLs if those are inside a text area.
I have seen a similar question has been asked before here: Rails: Using a Textarea for :has_many relationship , but I fail to see how would I code the view and model in order to do so.
So, if I have this:
Order has_many Pages
And a form like this:
<%= form_for #order do |f| %>
<%= f.text_area :page_urls?? %> # This would let the user paste X URLs, which would be
# used to create X Pages associated with the Order.
<% end %>
You could retain the view code that you have:
<%= form_for #order do |f| %>
<%= f.text_area :page_urls %>
#other field and submit button
<% end %>
In your model, you'll need to do the following:
attr_accessor :page_urls
after_validation do
if page_urls
parse_page_urls.each do |url|
pages.create(url: url)
end
end
end
def parse_page_urls
#use regexp to extract urls from page_urls string and return an array of url strings
end
The accessor is defined so that you can use :page_urls in your form_builder. You could set easily validations in your model for :page_urls that way too.
Once order has been validated, it will create page objects according to the number of urls extracted from the page_urls attribute.
You could refer to this for some help with using regexp to extract the urls from the string.
Hope that helps!
This is a job best handled with nested form. It will let you submit attributes of a has_many relationship model from the parent model, like you wish to do. For example, from its docs:
Imagine you have a Project model that has_many :tasks. To be able to use this gem, you'll need to add accepts_nested_attributes_for :tasks to your Project model. If you wish to allow the nested objects to be destroyed, then add the :allow_destroy => true option to that declaration. See the accepts_nested_attributes_for documentation for details on all available options.
This will create a tasks_attributes= method, so you may need to add it to the attr_accessible array (attr_accessible :tasks_attributes).
Then use the nested_form_for helper method to enable the nesting.
<%= nested_form_for #project do |f| %>
You will then be able to use link_to_add and link_to_remove helper methods on the form builder in combination with fields_for to dynamically add/remove nested records.
<%= f.fields_for :tasks do |task_form| %>
<%= task_form.text_field :name %>
<%= task_form.link_to_remove "Remove this task" %>
<% end %>
<%= f.link_to_add "Add a task", :tasks %>
In response to your comment:
In order to do something like that, you would need to do processing in the controller to separate the URL's, then make a new Page object associated with #order object. Unfortunately, there isn't a way to do this without post-processing, unless you do it with JS on the client side with hidden inputs.

Using non associated models data in a form

Is it possible to access another models attributes without having associations? For example I want to create a Prediciton record via a form using the fixture models attributes
<%= form_for #prediction do |f| %>
<%= f.fields_for :fixtures, #fixtures do |builder| %>
<%= builder.text_field :home_team %> VS <%= builder.text_field :away_team %><%= f.text_field :home_score %><%= f.text_field :away_score %><br>
<% end %>
<% end %>
how would i get the attributes of the fixture model without associating the two models?
Thanks
It's much easier if you create the association. If you are not going to create the association, such in the case where you are using a view not backed by a model and your are pulling in and modifying various models from it (assumption I am making) you can do something similar to this:
First make sure you setup routes.rb for whichever methods you are planning to use against the various models.
predictions model
#fixtures = Fixture.all
or specific attributes example
#fixtures = Fixture.select([:home_team, :away_team, :home_score, :away_score]).all
The above is if you are updating another models records. You will also need to modify the create method.
Your view you would want to change from a form_for to a form_tag:
form_tag('/predictions') do
Hopefully this gets you going in the right direction.

Many-to-many associations and REST?

Newbie question, you've been warned!
I'm trying to implement a sample Rails app with a many-to-many association, people owning movies, and I'm trying to figure out how exactly to implement the UI for it. It's my understanding that REST requires everything to be a resource, so in this case "User" (person), "Movie" and "Possession" (the joint table) (oh, the puns).
Now the interesting part, the UX. Let's say I have a user dashboard where all of your movies are listed.
Let's say the user wants to add a movie that he owns. How do you do this in REST? It's trivial with a custom action that one could add to the User controller, but the point is not to go beyond the basic 7 REST actions, right? Therefore I'd have to first do a "new" on a movie and then do a "new" on a possession, which are two operations. How do I collapse them into one?
Basically I feel I'm not quite understanding how to maintain REST as soon as multiple models are involved and would appreciate a tip.
Thanks!
Happily, Rails has some magic just for this common scenario. Assuming a model like this:
class Movie
has_many :users, :through => :possessions
end
Your view:
<%= form_for [current_user, Movie.new] do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<% end %>
Basically this form will POST to MoviesController#create and will pass along current_user.id as a user_id parameter that (the default) MoviesController#create will know to associate with the Movie it creates. Take a look at the documentation for FormBuilder#form_for for more information.
You could also do this the other way around, by the way:
class User
has_many :movies, :through => :possessions
accepts_nested_attributes_for :movies # magic!
end
And the view:
<%= form_for current_user |user_form| %>
<%= user_form.fields_for current_user.movies.build |movie_fields| %>
<%= movie_fields.label :title %>
<%= movie_fields.text_field :title %>
<% end %>
<% end %>
In this case the form will submit to UsersController#update and its parameters will look like this:
{ :id => 123,
:movie => {
:title => "The Red Balloon"
}
}
...and the controller will know to create the Movie object. For more information check the documentation for FormHelper#fields_for.

how to handle multiple models in a rails form

http://weblog.rubyonrails.org/2009/1/26/nested-model-forms
This post helped in learning how to handle multiple models in a rails form. It works as long as the models are nested. what if they are not? lets say, I have a form, where the user fills personal details, address details and a bunch of checkboxes specifying her interests. There are at least 3 tables involved in this one single form, what is the best way to handle this, without having 3 different save buttons?
Two options:
First is ActivePresenter which works well for this.
Second is just to use fields_for:
<%= form_for #user do |f| %>
<%=f.label :name %>
<%=f.text_field :name %>
<%= fields_for #address do |fa| %>
<%=fa.label :city %>
<%=fa.text_field :city %>
<% end %>
<% end %>
Then in the controller, save the records.
#user = User.new(params[:user])
#address = Address.new(params[:address])
ActivePresenter works so well though.
Also found a railsforum post via Google, which would work well.
You can refer this tutorial by The Pragmatic Programmers
Advanced Rails Recipes

Resources