NoMethodError within nested model form - ruby-on-rails

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!

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

Related

Rails 7 Dynamic Nested Forms with hotwire/turbo frames?

I'm very new to the rails. I've started right from rails7 so there is still very little information regarding my problem.
Here is what i have:
app/models/cocktail.rb
class Cocktail < ApplicationRecord
has_many :cocktail_ingredients, dependent: :destroy
has_many :ingredients, through: :cocktail_ingredients
accepts_nested_attributes_for :cocktail_ingredients
end
app/models/ingredient.rb
class Ingredient < ApplicationRecord
has_many :cocktail_ingredients
has_many :cocktails, :through => :cocktail_ingredients
end
app/models/cocktail_ingredient.rb
class CocktailIngredient < ApplicationRecord
belongs_to :cocktail
belongs_to :ingredient
end
app/controllers/cocktails_controller.rb
def new
#cocktail = Cocktail.new
#cocktail.cocktail_ingredients.build
#cocktail.ingredients.build
end
def create
#cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
if #cocktail.save
format.html { redirect_to cocktail_url(#cocktail), notice: "Cocktail was successfully created." }
format.json { render :show, status: :created, location: #cocktail }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: #cocktail.errors, status: :unprocessable_entity }
end
end
end
def cocktail_params
params.require(:cocktail).permit(:name, :recipe, cocktail_ingredients_attributes: [:quantity, ingredient_id: []])
end
...
db/seeds.rb
Ingredient.create([ {name: "rum"}, {name: "gin"} ,{name: "coke"}])
relevant tables from schema
create_table "cocktail_ingredients", force: :cascade do |t|
t.float "quantity"
t.bigint "ingredient_id", null: false
t.bigint "cocktail_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cocktail_id"], name: "index_cocktail_ingredients_on_cocktail_id"
t.index ["ingredient_id"], name: "index_cocktail_ingredients_on_ingredient_id"
end
create_table "cocktails", force: :cascade do |t|
t.string "name"
t.text "recipe"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
...
add_foreign_key "cocktail_ingredients", "cocktails"
add_foreign_key "cocktail_ingredients", "ingredients"
app/views/cocktails/_form.html.erb
<%= form_for #cocktail do |form| %>
<% if cocktail.errors.any? %>
<% cocktail.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name, value: "aa"%>
</div>
<div>
<%= form.label :recipe, style: "display: block" %>
<%= form.text_area :recipe, value: "nn" %>
</div>
<%= form.simple_fields_for :cocktail_ingredients do |ci| %>
<%= ci.collection_check_boxes(:ingredient_id, Ingredient.all, :id, :name) %>
<%= ci.text_field :quantity, value: "1"%>
<% end %>
<div>
<%= form.submit %>
</div>
<% end %>
Current error:
Cocktail ingredients ingredient must exist
What I'm trying to achieve:
I want a partial where I can pick one of the 3 ingredients and enter its quantity. There should be added/remove buttons to add/remove ingredients.
What do i use? Turbo Frames? Hotwire? How do i do that?
Im still super confused with everything in rails so would really appreciate in-depth answer.
1. Controller & Form - set it up as if you have no javascript,
2. Turbo Frame - then wrap it in a frame.
3. TLDR - if you don't need a long explanation.
4. Turbo Stream - you can skip Turbo Frame and do this instead.
5. Bonus - make a custom form field
6. Frame + Stream - i didn't know you can do that
Controller & Form
To start, we need a form that can be submitted and then re-rendered without creating a new cocktail.
Using accepts_nested_attributes_for does change the behavior of the form, which is not obvious and it'll drive you insane when you don't understand it.
First, lets fix the form. I'll use the default rails form builder, but it is the same setup with simple_form as well:
<!-- form_for or form_tag: https://guides.rubyonrails.org/form_helpers.html#using-form-tag-and-form-for
form_with does it all -->
<%= form_with model: cocktail do |f| %>
<%= (errors = safe_join(cocktail.errors.map(&:full_message).map(&tag.method(:li))).presence) ? tag.div(tag.ul(errors), class: "prose text-red-500") : "" %>
<%= f.text_field :name, placeholder: "Name" %>
<%= f.text_area :recipe, placeholder: "Recipe" %>
<%= f.fields_for :cocktail_ingredients do |ff| %>
<div class="flex gap-2">
<div class="text-sm text-right"> <%= ff.object.id || "New ingredient" %> </div>
<%= ff.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= ff.text_field :quantity, placeholder: "Qty" %>
<%= ff.check_box :_destroy, title: "Check to delete ingredient" %>
</div>
<% end %>
<!-- NOTE: Form has to be submitted, but with a different button,
that way we can add different functionality in the controller
see `CocktailsController#create` -->
<%= f.submit "Add ingredient", name: :add_ingredient %>
<div class="flex justify-end p-4 border-t bg-gray-50"> <%= f.submit %> </div>
<% end %>
<style type="text/css" media="screen">
input[type], textarea, select { display: block; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; width: 100%; border: 1px solid rgba(0,0,0,0.15); border-radius: .375rem; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px }
input[type="checkbox"] { width: auto; padding: 0.75rem; }
input[type="submit"] { width: auto; cursor: pointer; color: white; background-color: rgb(37, 99, 235); font-weight: 500; }
</style>
https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for
We need a single ingredient per cocktail_ingredient as indicated by belongs_to :ingredient. Single select is an obvious choice; collection_radio_buttons also applicable.
fields_for helper will output a hidden field with an id of cocktail_ingredient if that particular record has been persisted in the database. That's how rails knows to update existing records (with id) and create new records (without id).
Because we're using accepts_nested_attributes_for, fields_for appends "_attributes" to the input name. In other words, if you have this in your model:
accepts_nested_attributes_for :cocktail_ingredients
that means
f.fields_for :cocktail_ingredients
will prefix input names with cocktail[cocktail_ingredients_attributes].
(WARN: source code incoming) The reason is because accepts_nested_attributes_for has defined a new method cocktail_ingredients_attributes=(params) in Cocktail model, which does a lot of work for you. This is where nested parameters are handled, CocktailIngredient objects are created and assigned to corresponding cocktail_ingredients association and also marked to be destroyed if _destroy parameter is present and because autosave is set to true, you get automatic validations. This is just an FYI, in case you want to define your own cocktail_ingredients_attributes= method and you can and f.fields_for will pick it up automatically.
In CocktailsController, new and create actions need a tiny update:
# GET /cocktails/new
def new
#cocktail = Cocktail.new
# NOTE: Because we're using `accepts_nested_attributes_for`, nested fields
# are tied to the nested model now, a new object has to be added to
# `cocktail_ingredients` association, otherwise `fields_for` will not
# render anything; (zero nested objects = zero nested fields).
#cocktail.cocktail_ingredients.build
end
# POST /cocktails
def create
#cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
# NOTE: Catch when form is submitted by "add_ingredient" button;
# `params` will have { add_ingredient: "Add ingredient" }.
if params[:add_ingredient]
# NOTE: Build another cocktail_ingredient to be rendered by
# `fields_for` helper.
#cocktail.cocktail_ingredients.build
# NOTE: Rails 7 submits as TURBO_STREAM format. It expects a form to
# redirect when valid, so we have to use some kind of invalid
# status. (this is temporary, for educational purposes only).
# https://stackoverflow.com/a/71762032/207090
# NOTE: Render the form again. TADA! You're done.
format.html { render :new, status: :unprocessable_entity }
else
if #cocktail.save
format.html { redirect_to cocktail_url(#cocktail), notice: "Cocktail was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
end
In Cocktail model allow the use of _destroy form field to delete record when saving:
accepts_nested_attributes_for :cocktail_ingredients, allow_destroy: true
That's it, the form can be submitted to create a cocktail or submitted to add another ingredient.
Turbo Frame
Right now, when new ingredient is added the entire page is re-rendered by turbo. To make the form a little more dynamic, we can add turbo-frame tag to only update ingredients part of the form:
<!-- doesn't matter how you get the "id" attribute
it just has to be unique and repeatable across page reloads -->
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for :cocktail_ingredients do |ff| %>
<div class="flex gap-2">
<div class="text-sm text-right"> <%= ff.object&.id || "New ingredient" %> </div>
<%= ff.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= ff.text_field :quantity, placeholder: "Qty" %>
<%= ff.check_box :_destroy, title: "Check to delete ingredient" %>
</div>
<% end %>
</turbo-frame>
Change "Add ingredient" button to let turbo know that we only want the frame part of the submitted page. A regular link, doesn't need this, we would just put that link inside of the frame tag, but an input button needs extra attention.
<!-- same `id` as <turbo-frame>; repeatable, remember. -->
<%= f.submit "Add ingredient",
data: { turbo_frame: f.field_id(:ingredients)},
name: "add_ingredient" %>
Turbo frame id has to match the button's data-turbo-frame attribute:
<turbo-frame id="has_to_match">
<input data-turbo-frame="has_to_match" ...>
Now, when clicking "Add ingredient" button it still goes to the same controller, it still renders the entire page on the server, but instead of re-rendering the entire page (frame #1), only the content inside the turbo-frame is updated (frame #2). Which means, page scroll stays the same, form state outside of turbo-frame tag is unchanged. For all intents and purposes this is now a dynamic form.
Possible improvement could be to stop messing with create action and add ingredients through a different controller action, like add_ingredient:
# config/routes.rb
resources :cocktails do
post :add_ingredient, on: :collection
end
<%= f.submit "Add ingredient",
formmethod: "post",
formaction: add_ingredient_cocktails_path(id: f.object),
data: { turbo_frame: f.field_id(:ingredients)} %>
Add add_ingredient action to CocktailsController:
def add_ingredient
#cocktail = Cocktail.new(cocktail_params.merge({id: params[:id]}))
#cocktail.cocktail_ingredients.build # add another ingredient
# NOTE: Even though we are submitting a form, there is no
# need for "status: :unprocessable_entity".
# Turbo is not expecting a full page response that has
# to be compatible with the browser behavior
# (that's why all the status shenanigans; 422, 303)
# it is expecting to find the <turbo-frame> with `id`
# matching `data-turbo-frame` from the button we clicked.
render :new
end
create action can be reverted back to default now.
You could also reuse new action instead of adding add_ingredient:
resources :cocktails do
post :new, on: :new # add POST /cocktails/new
end
Full controller set up:
https://stackoverflow.com/a/72890584/207090
Then adjust the form to post to new instead of add_ingredient.
TLDR - Put it all together
I think this is as simple as I can make it. Here is the short version (about 10ish extra lines of code to add dynamic fields, and no javascript)
# config/routes.rb
resources :cocktails do
post :add_ingredient, on: :collection
end
# app/controllers/cocktails_controller.rb
# the other actions are the usual default scaffold
def add_ingredient
#cocktail = Cocktail.new(cocktail_params.merge({id: params[:id]}))
#cocktail.cocktail_ingredients.build
render :new
end
# app/views/cocktails/new.html.erb
<%= form_with model: cocktail do |f| %>
<%= (errors = safe_join(cocktail.errors.map(&:full_message).map(&tag.method(:li))).presence) ? tag.div(tag.ul(errors), class: "prose text-red-500") : "" %>
<%= f.text_field :name, placeholder: "Name" %>
<%= f.text_area :recipe, placeholder: "Recipe" %>
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for :cocktail_ingredients do |ff| %>
<div class="flex gap-2">
<div class="text-sm text-right"> <%= ff.object&.id || "New ingredient" %> </div>
<%= ff.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= ff.text_field :quantity, placeholder: "Qty" %>
<%= ff.check_box :_destroy, title: "Check to delete ingredient" %>
</div>
<% end %>
</turbo-frame>
<%= f.button "Add ingredient", formmethod: "post", formaction: add_ingredient_cocktails_path(id: f.object), data: { turbo_frame: f.field_id(:ingredients)} %>
<div class="flex justify-end p-4 border-t bg-gray-50"> <%= f.submit %> </div>
<% end %>
# app/models/*
class Cocktail < ApplicationRecord
has_many :cocktail_ingredients, dependent: :destroy
has_many :ingredients, through: :cocktail_ingredients
accepts_nested_attributes_for :cocktail_ingredients, allow_destroy: true
end
class Ingredient < ApplicationRecord
has_many :cocktail_ingredients
has_many :cocktails, through: :cocktail_ingredients
end
class CocktailIngredient < ApplicationRecord
belongs_to :cocktail
belongs_to :ingredient
end
Turbo Stream
Turbo stream is as dynamic as we can get with this form without touching any javascript. The form has to be changed to let us render a single cocktail ingredient:
# NOTE: remove `f.submit "Add ingredient"` button
# and <turbo-frame> with nested fields
# NOTE: this `id` will be the target of the turbo stream
<%= tag.div id: :cocktail_ingredients do %>
<%= f.fields_for :cocktail_ingredients do |ff| %>
# put nested fields into a partial
<%= render "ingredient_fields", f: ff %>
<% end %>
<% end %>
# NOTE: `f.submit` is no longer needed, because there is no need to
# submit the form anymore just to add an ingredient.
<%= link_to "Add ingredient",
add_ingredient_cocktails_path,
class: "text-blue-500 hover:underline",
data: { turbo_method: :post } %>
# ^
# NOTE: still has to be a POST request
<!-- app/views/cocktails/_ingredient_fields.html.erb -->
<div class="flex gap-2">
<div class="text-sm text-right"> <%= f.object&.id || "New" %> </div>
<%= f.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= f.text_field :quantity, placeholder: "Qty" %>
<%= f.check_box :_destroy, title: "Check to delete ingredient" %>
</div>
Update add_ingredient action to render a turbo_stream response:
# it should be in your routes, see previous section above.
def add_ingredient
# NOTE: get a form builder but skip the <form> tag, `form_with` would work
# here too. however, we'd have to use `fields` if we were in a template.
helpers.fields model: Cocktail.new do |f|
# NOTE: instead of letting `fields_for` helper loop through `cocktail_ingredients`
# we can pass a new object explicitly.
# v
f.fields_for :cocktail_ingredients, CocktailIngredient.new, child_index: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) do |ff|
# ^ ^ Time.now.to_f also works
# NOTE: one caveat is that we need a unique key when we render this
# partial otherwise it would always be 0, which would override
# previous inputs. just look at the generated input `name` attribute:
# cocktail[cocktail_ingredients_attributes][0][ingredient_id]
# ^
# we need a different number for each set of fields
render turbo_stream: turbo_stream.append(
"cocktail_ingredients",
partial: "ingredient_fields",
locals: { f: ff }
)
end
end
end
# NOTE: `fields_for` does output an `id` field for persisted records
# which would be outside of the rendered html and turbo_stream.
# not an issue here since we only render new records and there is no `id`.
Bonus - Custom Form Builder
Making a custom field helper simplifies the task down to one line:
# config/routes.rb
# NOTE: I'm not using `:id` for anything, but just in case you need it.
post "/fields/:model(/:id)/build/:association(/:partial)", to: "fields#build", as: :build_fields
# app/controllers/fields_controller.rb
class FieldsController < ApplicationController
# POST /fields/:model(/:id)/build/:association(/:partial)
def build
resource_class = params[:model].classify.constantize # => Cocktail
association_class = resource_class.reflect_on_association(params[:association]).klass # => CocktailIngredient
fields_partial_path = params[:partial] || "#{association_class.model_name.collection}/fields" # => "cocktail_ingredients/fields"
render locals: { resource_class:, association_class:, fields_partial_path: }
end
end
# app/views/fields/build.turbo_stream.erb
<%=
fields model: resource_class.new do |f|
turbo_stream.append f.field_id(params[:association]) do
f.fields_for params[:association], association_class.new, child_index: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) do |ff|
render fields_partial_path, f: ff
end
end
end
%>
# app/models/dynamic_form_builder.rb
class DynamicFormBuilder < ActionView::Helpers::FormBuilder
def dynamic_fields_for association, name = nil, partial: nil, path: nil
association_class = object.class.reflect_on_association(association).klass
partial ||= "#{association_class.model_name.collection}/fields"
name ||= "Add #{association_class.model_name.human.downcase}"
path ||= #template.build_fields_path(object.model_name.name, association:, partial:)
#template.tag.div id: field_id(association) do
fields_for association do |ff|
#template.render(partial, f: ff)
end
end.concat(
#template.link_to(name, path, class: "text-blue-500 hover:underline", data: { turbo_method: :post })
)
end
end
This new helper requires "#{association_name}/_fields" partial:
# app/views/cocktail_ingredients/_fields.html.erb
<%= f.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= f.text_field :quantity, placeholder: "Qty" %>
<%= f.check_box :_destroy, title: "Check to delete ingredient" %>
Override the default form builder and now you should have dynamic_fields_for input:
# app/views/cocktails/_form.html.erb
<%= form_with model: cocktail, builder: DynamicFormBuilder do |f| %>
<%= f.dynamic_fields_for :cocktail_ingredients %>
<%# f.dynamic_fields_for :other_things, "Add a thing", partial: "override/partial/path" %>
# or without dynamic form builder, just using the new controller
<%= tag.div id: f.field_id(:cocktail_ingredients) %>
<%= link_to "Add ingredient", build_fields_path(:cocktail, :cocktail_ingredients), class: "text-blue-500 hover:underline", data: { turbo_method: :post } %>
<% end %>
Frame + Stream
You can render turbo_stream tag on the current page and it will work. Pretty useless to render something just to move it somewhere else on the same page. But, if we put it inside a turbo_frame, we can move things outside of the frame for safekeeping while getting updates inside the turbo_frame.
# app/controllers/cocktails_controller.rb
# GET /cocktails/new
def new
#cocktail = Cocktail.new
#cocktail.cocktail_ingredients.build
# turbo_frame_request? # => true
# request.headers["Turbo-Frame"] # => "add_ingredient"
# skip `new.html.erb` rendering if you want
render ("_form" if turbo_frame_request?), locals: { cocktail: #cocktail }
end
# app/views/cocktails/_form.html.erb
<%= tag.div id: :ingredients %>
<%= turbo_frame_tag :add_ingredient do %>
# NOTE: render all ingredients and move them out of the frame.
<%= turbo_stream.append :ingredients do %>
# NOTE: just need to take extra care of that `:child_index` and pass it as a proc, so it would be different for each object
<%= f.fields_for :cocktail_ingredients, child_index: -> { Process.clock_gettime(Process::CLOCK_REALTIME, :microsecond) } do |ff| %>
<%= ff.select :ingredient_id, Ingredient.all.map { |i| [i.name, i.id] }, include_blank: "Select ingredient" %>
<%= ff.text_field :quantity, placeholder: "Qty" %>
<%= ff.check_box :_destroy, title: "Check to delete ingredient" %>
<% end %>
<% end %>
# NOTE: this link is inside `turbo_frame`, so if we navigate to `new` action
# we get a single set of new ingredient fields and `turbo_stream`
# moves them out again.
<%= link_to "Add ingredient", new_cocktail_path, class: "text-blue-500 hover:underline" %>
<% end %>
No extra actions, controllers, routes, partials or responses. Just a GET request with Html response, and only a single set of fields gets appended. I didn't see this explained anywhere, sure hope that's the expected behavior.

Rails/ActiveRecord - association not saving

I can't get my CheckIn record to save because the associated Tenancy isn't saving.
I have three models with associations:
class Property < ApplicationRecord
has_many :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :property
has_many :check_ins
end
class CheckIn < ApplicationRecord
belongs_to :tenancy
accepts_nested_attributes_for :tenancy
end
I want the CheckIn new action to create both the CheckIn and the associated Tenancy:
def new
#check_in = CheckIn.new
#check_in.build_tenancy.property_id = params[:property_id]
end
I have to include the property_id part otherwise the Tenancy won't save.
The form in check_ins/new.html.erb:
<%= form_for #check_in, url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I've added tenancy attributes to the strong params in the CheckInsController:
def check_in_params
params.require(:check_in).permit(:tenancy_id, :date_time, tenancy_attributes: [:start_date])
end
It's worth noting that the check_ins routes are nested in properties:
resources :properties do
resources :check_ins, only: [:new, :create]
end
So the problem is that by the time I get to the create action in the CheckInsController, the tenancy that I built has disappeared. I'm not sure how and when each of the records should be being saved and the slight complexity of what I'm trying to achieve has made it quite difficult to find relevant help so any ideas?
I'm using Rails 5.
The problem was that the property attached to the tenancy was being forgotten. I removed the property attachment from the new action:
def new
#check_in = CheckIn.new
#check_in.build_tenancy
end
Added a hidden field for property_id to the form (as well as adding :property_id to the strong params):
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<%= i.hidden_field :property_id, value: params[:property_id] %>
<% end %>
And saved the tenancy in the CheckIn create action, prior to saving the check in itself:
def create
#check_in = CheckIn.new(check_in_params)
#check_in.tenancy.save
if #check_in.save
redirect_to property_check_in_path(#check_in.tenancy.property.id, #check_in)
else
render :new
end
end
I'd certainly be interested if anyone could pick holes in this solution or offer a better one.
Using nested resources (check_ins depends from properties) you create a namespaces routes. form_for helper ( rails guides - form helpers ) when you build your form, need a Property reference also.
I try to explain me better with an example:
#checks_controller.rb
def new
#property = Property.new
#check_in = #property.build_check_ins
#check_in.build_tenancy
end
#check_ins/new.html.erb
<%= form_for [#property, #check_in], url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I haven't tried this code, but I hope this give you at least a way to follow to solve your problem.

Nested Attributes Child model don't save

I'm having a problem in the model saving with nested attributes.
In the app, there's a Customer, that have 1..n Contacts witch in turn have 1..n Telephones.
I've searched a lot before asking here, and decided to make it save only the Contact first. Well, at first the Customer is stored, but Contact is not. From what I read there's no need to repeat the ... contacts.build from new function in the create, and that the line "#customer = Customer.new(customer_params)" would create and store them both.
Why it's not working? (That's the first question.)
After some modifications and debugging, I found that when I set a second line building Contact (...contacts.build(customer_params[:contacts_attributes])) it's not saved because of an error of 'unknown attribute'. That's because between the hash :contacts_attribute and the content of it, it's added another hash, called ':0' (?). The structure of the hash that comes from the form is this :
":contacts_attribute[:0[:name, :department, :email]]"
I imagine that this hash :0 is for adding more than one Contact instance, that will come in hashes :1, :2 etc.
There's a way to store the Contact instance by getting this :0 hash? (How do I access this hash? Is it "... :contacts_attribute[0]"?)
Below is the relevant code.
Thanks for the attention!
customer.rb
class Customer < ActiveRecord::Base
...
has_many :contacts
accepts_nested_attributes_for :contacts, reject_if: lambda {|attributes| attributes['kind'].blank?}
...
def change_by(user_id)
update_attributes(changed_by: user_id, deleted_at: Time.now, updated_at: Time.now)
end
def delete(user_id)
update_attributes(status: false, changed_by: user_id, deleted_at: Time.now, updated_at: Time.now)
end
private
...
end
customers_controller.rb
class CustomersController < ApplicationController
def new
#customer = Customer.new
#customer.contacts.new
end
def create
user_id = session[:user_id]
#customer = Customer.new(customer_params)
if #customer.save
#customer.change_by(user_id)
flash[:success] = "Cliente cadastrado com sucesso!"
redirect_to customers_url
else
render 'new'
end
end
private
def customer_params
params.require(:customer).permit(:razao_social, :nome, :CPF_CNPJ,
:adress_id, :email_nota, :transporter_id, :observacao,
contacts_attributes: [:nome, :setor, :email])
end
Form
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for #customer do |f| %>
<%= f.label "Dados Básicos" %>
<div class="well">
<%= f.label :razao_social, "Razão Social" %>
<%= f.text_field :razao_social %>
<%= f.label :nome, "Nome" %>
<%= f.text_field :nome %>
<%= f.label :CPF_CNPJ, "CPF/CNPJ" %>
<%= f.text_field :CPF_CNPJ %>
<%= f.label :email_nota, "Email para nota" %>
<%= f.email_field :email_nota %>
<%= f.label :observacao, "Observações" %>
<%= f.text_area :observacao %>
</div>
<%= f.fields_for :contacts do |k| %>
<%= k.label "Contato" %>
<div class="well">
<%= k.label :nome, "Nome" %>
<%= k.text_field :nome %>
<%= k.label :setor, "Setor" %>
<%= k.text_field :setor %>
<%= k.label :email, "Email" %>
<%= k.email_field :email %>
</div>
<% end %>
<%= f.submit "Cadastrar Cliente", class: "btn btn-primary" %>
<% end %>
</div>
reject_if: lambda {|attributes| attributes['kind'].blank?}
No sign of :kind in your form or your customer_params
This might have something to do with it.
Other than that, if you need an add/remove relationship for contacts, check out the cocoon gem. If you only need one, then build that into your fields for:
<%= f.fields_for :contacts, #customer.contacts.first || #customer.contacts.build do |k| %>
The form will then be specific to a single instance of contact.
There's a way to store the Contact instance by getting this :0 hash?
(How do I access this hash? Is it "... :contacts_attribute[0]"?)
You don't need to access it, that's what the accepts_nested_attributes is for. The rest of your code looks ok so sort out the rejection issue at the top and come back if there are still problems, and post the log output - specifically the params hash for the request!

Ruby on Rails: Can't implement Star Rating system

I'm trying to add a simple Star Rating system for my app having taken this tutorial for an example. I have User, Hotel and Rating models. Dependencies are:
(rating.rb)
belongs_to :user
belongs_to :hotel
(hotel.rb) & (user.rb)
has_many :ratings
And with the following code in hotel view I get this error:
NameError in Hotels#show
undefined local variable or method `user' for Class...
(in the line with <%= form_for...)
Hotel view (show.html.erb):
<% form_id = "hotel_#{#hotel.id}_rating" %>
<% if signed_in? %> <!-- To avoid throwing an exception if no user is signed in -->
<% user_id = current_user.id %>
<% else %>
<% user_id = -1 %>
<% end %>
<%= form_for #hotel.ratings.find_or_create_by_user_id user.id,
:html => {:id => form_id,
:class => "star_rating_form"} do |f| %>
<%= f.hidden_field :hotel_id, :value => #hotel.id %>
<% if signed_in? %>
<%= f.hidden_field :user_id, :value => current_user.id %>
<% end %>
<%= f.hidden_field :stars, :id => form_id + "_stars" %>
<% end %>
<% (1..5).each do |i| %>
<li class="rating_star" id="<%= form_id %>_<%= i %>" data-stars="<%= i %>" data-form-id="<%= form_id %>"></li>
<% end %>
Ratings controller is:
def create
end
def update
end
def rating_params
params.require(:rating).permit(:stars)
end
Migration file is:
create_table :ratings do |t|
t.integer :stars, :default => 0
t.references :store
t.references :user
end
From the comments, the error seems to be here:
#hotel.ratings.find_or_create_by_user_id user.id
--
user_id
The problem is your show view doesn't have access to a local variable called user
This variable should either be defined in the controller (which would mean it has to be an #instance variable, or should be a helper (such as current_user.id)
The fix should therefore be as follows:
<% user_id = user_signed_in? ? current_user.id : "-1" %>
<%= form_for #hotel.ratings.find_or_create_by_user_id user_id ...
This should get it working for you with the code you have provided. As you've not provided the new action from the controller, I don't know whether the supporting structure for the code will be correct or not.
After some search on find_or_create_by, I changed line with 'form_for' into
<%= form_for #hotel.ratings.find_or_create_by(user_id: user_id)
That solved the issue!
Thanks to all for your support!

Different type of questions on a form

I am trying to build an app where people can come and register to some programs.
Through the application form, I need to have different kinds of questions, some text fields, some text areas, some selects, and some check boxes.
For now, I have created a model question with a content. I was thinking of creating a question type model or different models (one for each type of question) so that when creating the form, the user could choose the type of the question he wants to ask.
Here is the question model :
class Question < ActiveRecord::Base
belongs_to :post
end
The question table :
create_table "questions", force: true do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.integer "post_id"
t.text "content"
end
For now, in the _form.rb :
<%= form_for #post do |f| %>
<%= f.fields_for :questions do |qform| %>
<% render 'question_fields', :f => qform %>
<% end %>
<p><%= link_to_add_fields "Add text area", f, :questions %></p>
<% end %>
_question_fields.rb :
<p>
<%= f.label :content, "Question" %>
<%= f.text_area :content %><br>
<%= f.check_box '_destroy' %>
</p>
And the javascript functions are :
In application_helper.rb :
def link_to_add_fields(name, f, association)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render(association.to_s.singularize + "_fields", :f => builder)
end
link_to_function(name, "add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")")
end
In application.js :
function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp("new_" + association, "g")
$(link).parent().before(content.replace(regexp, new_id));
}
How can it be done ?
If you want to develop surveys, questionnaires, quizzes.. Then please try Surveyor Gem.
It let you to generate questionaire in your application. Have a look on this :
https://github.com/NUBIC/surveyor

Resources