0

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!

  • What's the actual error? Can you put it in your question? NoMethod what for what? – Sasha Dec 18 '13 at 05:54
  • Also, what lines of what files does it implicate further down the stack trace? It's probably something in the link_to_add_fields method, because you're feeding that method the right number of arguments, so the problem isn't likely in your call of the method. – Sasha Dec 18 '13 at 06:29
  • Just to be sure, is your error with the adding of new elements or the processing of current ones? – Richard Peck Dec 18 '13 at 09:49
  • Apologies for replying late. @Sasha - error: NoMethodError in Plans#show stack trace: app/helpers/application_helper.rb:3:in `link_to_add_fields' app/views/plans/_exercise_fields.html.erb:2:in `_app_views_plans__exercise_fields_html_erb___638795321783742101_70113892635260' app/helpers/application_helper.rb:6:in `block in link_to_add_fields' app/helpers/application_helper.rb:5:in `link_to_add_fields' app/views/plans/_form.html.erb:22:in `block in _app_views_plans__form_html_erb__4174597632972162822_70113896061880' app/views/plans/_form.html.erb:1:in – Kristian Bouw Dec 18 '13 at 15:08
  • @RichPeck it's pertaining to the addition of new elements. – Kristian Bouw Dec 18 '13 at 15:10
  • That's a killer - you will benefit from the [link](http://pikender.in/2013/04/20/child-forms-using-fields_for-through-ajax-rails-way/) I included in my huuuuuuge answer – Richard Peck Dec 18 '13 at 15:15

1 Answers1

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

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • Amazing in-depth answer. I can't thank you enough for taking your time to answer this question. I'm about to implement this now and I'll comment back if I have any issues. – Kristian Bouw Dec 18 '13 at 15:05
  • Rich, in the second part where you mention 2 partials need to be created, the second one seems to have been cut off maybe? Correct me if I'm wrong, thanks. – Kristian Bouw Dec 18 '13 at 15:17
  • Sorry, let me fix for you! – Richard Peck Dec 18 '13 at 15:51
  • All fixed for you bud – Richard Peck Dec 18 '13 at 15:52
  • Made some fixes to the file names - some big errors I didn't see before! – Richard Peck Dec 18 '13 at 16:04
  • The only issue i'm running into now is that I keep receiving this error after implementing the plans.js.coffee file you suggested: "ExecJS::RuntimeError in Plans#show". I installed node.js and when I delete the plans.js.coffee content everything runs fine. It's only when I add your suggested code that I receive the error. Thoughts? – Kristian Bouw Dec 18 '13 at 16:51
  • Can you elaborate more about your installation of nodejs? The problem might be a syntax error with the ajax (I removed some stuff that didn't need to be there). Also, which V of rails are you using? You've mentioned ROR3 & ROR4 in your tags ;) – Richard Peck Dec 18 '13 at 17:03
  • The application works when I delete the "//= require_tree ." from my application.js file, although I feel this is probably not a good thing. Also, how am I able to add a button to my plan page to allow people to select "Add Exercise" and have the element dynamically added to the page? (Similar to how railscast allows you to add answers to a question) – Kristian Bouw Dec 18 '13 at 17:06
  • It's rails 4, ruby 2. This is the code being highlighted in my application.js file: "<%= javascript_include_tag "application", "data-turbolinks-track" => true %>". For node.js, I installed it on my computer, added the 'execjs' gem to my gemfile, and included "ENV['EXECJS_RUNTIME'] = 'Node'" in my config/boot.rb file [(as per this guide)](http://ajacevedo.com/2013/using-node-js-as-a-rails-javascript-runtime/) – Kristian Bouw Dec 18 '13 at 17:08
  • Hmm okay. Are you sure that nodeJS isn't interfering with the code? Do you have any more `coffee` files in your javascripts folder? We can try putting the code into `application.js` to see if that helps – Richard Peck Dec 18 '13 at 17:14
  • I only had an additional pages.js.coffee but it's empty. I tried putting the code into the application.js to see if that fixed anything but it didn't. What i'm going to do is delete my working branch for this feature and start fresh following what you've provided (excluding node.js from my gemfile and boot.rb). I'll comment back with any issues I run into. Thanks again for all the help and assistance! (I wish I could give you a gold ribbon) – Kristian Bouw Dec 18 '13 at 17:42
  • No problem bud! It's okay - I hope you learn something from it :) – Richard Peck Dec 18 '13 at 18:09