问题
The project is a simple workout creator where you can add exercises to a workout plan.
I've been following the Railscast covering nested model forms to allow dynamically adding and deleting exercises, but have run into an error and need a second opinion as a new developer.
The error I am continually receiving is: NoMethodError in Plans#show
This is the extracted code, with starred line the highlighted error:
<fieldset>
**<%= link_to_add_fields "Add Exercise", f, :exercise %>**
<%= f.text_field :name %>
<%= f.number_field :weight %>
<%= f.number_field :reps %>
Note: I have the exercise model created but not an exercise controller. An exercise can only exist in a plan but I was unsure if I still needed a create action in an exercise controller for an exercise to be added?
I followed the Railscast almost verbatim (the _exercise_fields partial I slightly deviated) so you're able to view my files against the ones he has shared in the notes.
My schema.rb
create_table "exercises", force: true do |t|
t.string "name"
t.integer "weight"
t.integer "reps"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "plan_id"
end
create_table "plans", force: true do |t|
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
end
My Plan model:
class Plan < ActiveRecord::Base
has_many :exercises
accepts_nested_attributes_for :exercises, allow_destroy: true
end
My Exercise model:
class Exercise < ActiveRecord::Base
belongs_to :plan
end
My _form.html.erb
<%= form_for @plan do |f| %>
<% if @plan.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@plan.errors.count, "error") %> prohibited this plan from being saved:</h2>
<ul>
<% @plan.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<%= f.fields_for :exercises do |builder| %>
<%= render 'exercise_fields', f: builder %>
<% end %>
<%= link_to_add_fields "Add Exercise", f, :exercises %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
My _exercise_fields.html.erb
<fieldset>
<%= link_to_add_fields "Add Exercise", f, :exercise %>
<%= f.text_field :name %>
<%= f.number_field :weight %>
<%= f.number_field :reps %>
<%= f.hidden_field :_destroy %>
<%= link_to "remove", '#', class: "remove_fields" %>
</fieldset>
My plans.js.coffee
jQuery ->
$('form').on 'click', '.remove_fields', (event) ->
$(this).prev('input[type=hidden]').val('1')
$(this).closest('fieldset').hide()
event.preventDefault()
$('form').on 'click', '.add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()
My application_helper.rb
module ApplicationHelper
def link_to_add_fields(name, f, association)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
end
I'm relatively new to programming so I apologize in advance if I have easily overlooked something. Any help, suggestions, or leads for sources to read up on my issue are greatly appreciated.
Thanks!
回答1:
Having implemented the functionality you seek, I'll give some ideas:
Accepts Nested Attributes For
As you already know, you can pass attributes from a parent to nested model by using the accepts_nested_attributes_for function
Although relatively simple, it's got a learning curve. So I'll explain how to use it here:
#app/models/plan.rb
Class Plan < ActiveRecord::Base
has_many :exercises
accepts_nested_attributes_for :exercises, allow_destroy: true
end
This gives the plan
model the "command" to send through any extra data, if presented correctly
To send the data correctly (in Rails 4), there are several important steps:
1. Build the ActiveRecord Object
2. Use `f.fields_for` To Display The Nested Fields
3. Handle The Data With Strong Params
Build The ActiveRecord Object
#app/controllers/plans_controller.rb
def new
@plan = Plan.new
@plan.exericses.build
end
Use f.fields_for To Display Nested Fields
#app/views/plans/new.html.erb
<%= form_for @plans do |f| %>
<%= f.fields_for :exercises do |builder| %>
<%= builder.text_field :example_field %>
<% end %>
<% end %>
Handle The Data With Strong Params
#app/controllers/plans_controller.rb
def create
@plan = Plan.new(plans_params)
@plan.save
end
private
def plans_params
params.require(:plan).permit(:fields, exerices_attributes: [:extra_fields])
end
This should pass the required data to your nested model. Without this, you'll not pass the data, and your nested forms won't work at all
Appending Extra Fields
Appending extra fields is the tricky part
The problem is that generating new f.fields_for
objects on the fly is only possible within a form
object (which only exists in an instance)
Ryan Bates gets around this by sending the current form
object through to a helper, but this causes a problem because the helper then appends the entire source code for the new field into a links' on click
event (inefficient)
We found this tutorial more apt
It works like this:
- Create 2 partials:
f.fields_for
&form
partial (for ajax) - Create new route (ajax endpoint)
- Create new controller action (to add extra field)
- Create JS to add extra field
Create 2 Partials
#app/views/plans/add_exercise.html.erb
<%= form_for @plan, :url => plans_path, :authenticity_token => false do |f| %>
<%= render :partial => "plans/exercises_fields", locals: {f: f, child_index: Time.now.to_i} %>
<% end %>
#app/views/plans/_exercise_fields.html.erb
<%= f.fields_for :exercises, :child_index => child_index do |builder| %>
<%= builder.text_field :example %>
<% end %>
Create New Route
#config/routes.rb
resources :plans do
collection do
get :add_exercise
end
end
Create Controller Action
#app/controllers/plans_controller.rb
def add_exercise
@plan = Plan.new
@plan.exercises.build
render "add_exericse", :layout => false
end
Create JS to Add The Extra Field
#app/assets/javascripts/plans.js.coffee
$ ->
$(document).on "click", "#add_exercise", (e) ->
e.preventDefault();
#Ajax
$.ajax
url: '/messages/add_exercise'
success: (data) ->
el_to_add = $(data).html()
$('#exercises').append(el_to_add)
error: (data) ->
alert "Sorry, There Was An Error!"
Apologies for the mammoth post, but it should work & help show you more info
来源:https://stackoverflow.com/questions/20649598/nomethoderror-within-nested-model-form