NoMethodError within nested model form

2019-06-14 01:45发布

问题:

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:

  1. Create 2 partials: f.fields_for & form partial (for ajax)
  2. Create new route (ajax endpoint)
  3. Create new controller action (to add extra field)
  4. 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