Save two models (which belong_to a third model) with one submit? - javascript

When the user clicks submit how can the info from two different models/DB tables be passed?
The user should be able to create a note in the missed_dates form and then that note should be saved to the respective #challenge the missed date is referring to.
missed_dates/form.html.erb
<%= simple_form_for(#missed_date, url: challenge_missed_dates_path({ routine_id: #challenge }), remote: request.xhr?, html: { data: { modal: true } }) do |a| %>
<%= form_for [#notable, #note] do |b| %>
<%= a.text_field :one %>
<%= b.text_field :two %>
<%= button_tag(type: 'submit') do %>
Save
<% end %>
<% end %>
<% end %>
missed_date.rb
class MissedDate < ActiveRecord::Base
belongs_to :user
belongs_to :challenge
end
missed_date_controller
def new
#challenge = current_user.challenges.find(params[:challenge_id])
#missed_date = current_user.missed_dates.build
#notable = #challenge
#note = Note.new
end
def create
challenge = current_user.challenges.find(params[:challenge_id])
challenge.missed_days = challenge.missed_days + 1
challenge.save
#missed_date = challenge.missed_dates.build(missed_date_params)
#missed_date.user = self.current_user
#missed_date.save
respond_modal_with #missed_date, location: root_path
flash[:alert] = 'Strike added'
end

Short: use "belongs_to" and "has_many :through" association between Note and MissedDates. Then you can use nested attributes.
Long version: This in probably an issue of an improper or incomplete structure of your models. Usually, you can use nested attributes (see http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html) to achieve this.
But, this implies that the models have a direct relation. You should consider if you can do a belongs_to/has_many relation between the note and the missed_date model. This could be done e.g. by "has_many :through..." (http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association) without changing your current db scheme.

Related

Create dynamic form with has_many (many-to-many) association with a single form in Rails 7

I have 4 models, User, Products, Property & ProductProperty. Here's how they relate:
All attributes in these models, such as name, value, upc, and available_on, must have a presence of value!
Once a user is created successfully, they can create products (with the Products model) that can have many properties (with the Property model) and a corresponding value (with the ProductProperty model). And I have to create all of these in a single form.
With the help of some tutorials and Reddit users, I was able to successfully implement the product form which is able to create a new product, property, and corresponding value. Here's the snap of my implementation:
Now here comes the tricky part: to add more properties dynamically to create multiple properties and their values of a product. So here's my implementation of it so far:
Product Form
<%= form_with model: #product do | prod | %>
<%= prod.label :name %>
<%= prod.text_field :name %>
<br>
<%= prod.label :upc, "UPC" %>
<%= prod.text_field :upc %>
<br>
<%= prod.label :available_on %>
<%= prod.date_field :available_on %>
<br>
<h3>Properties</h3>
<table>
<thead>
<tr>
<th>Property Value</th>
<th>Property Name</th>
</tr>
</thead>
<tbody class="fields">
<%= prod.fields_for :product_properties do | prod_prop | %>
<%= render "property", prod_prop: prod_prop %>
<% end %>
</tbody>
</table>
<br>
<%= link_to_add_fields('Add More Properties', prod, :product_properties) %>
<br>
<%= prod.submit %>
<% end %>
Property Form Partial
<tr>
<td><%= prod_prop.text_field :value %></td>
<%= prod_prop.fields_for :property do | prop | %>
<td><%= prop.text_field :name %></td>
<% end %>
</tr>
Helper Method
def link_to_add_fields(name, prod, association)
new_object = prod.object.send(association).klass.new
id = new_object.object_id
fields = prod.fields_for(association, new_object, child_index: :id) do | builder |
render "property", prod_prop: builder
end
link_to(name, "#", class: 'add_fields', data: {id: id, fields: fields.gsub("\n", "") } )
end
application.js file:
$("form").on("click", ".add_fields", function (event) {
let regexp, time;
time = new Date().getTime();
regexp = new RegExp($(this).data("id"), "g");
$(".fields").append($(this).data("fields").replace(regexp, time));
return event.preventDefault();
});
Now when I click on the "Add more Properties" link it adds only one field:
Issue 1: I believe I need to do some modifications in the helper method to implement this correctly but I am not able to figure out the logic of it.
There's one more issue! When we create additional property instances of property name & value, they will all disappear if there're any validation errors:
Product Controller:
def new
#product = Product.new
#product.product_properties.build.build_property
end
def create
#product = current_user.products.new(product_params)
if #product.valid?
#product.save
redirect_to products_path, notice: "Success!"
else
render 'new', status: :unprocessable_entity
end
end
I can think of a couple of solutions to this issue: first, to implement the logic in the create action for the dynamically created property name and value fields to persist between requests, second, to use javascript.
Issue 2: So how do I make the additional properties created by the user persist in the view between requests?
I am running rails v7.0.4 and I am not allowed to use hotwire, only Javascript.

How to link options to orders incl. the quantity of options?

How can I include product options to an order incl. the quantities for the respective options?
--> It is unclear to me how to deal with the many to many relationship between orders and product options and linking them all together in an order form.
Please find below some code on how my tables are linked at the moment and how I currently try to solve it in a form.
models
class Order < ApplicationRecord
has_many :order_options, dependent: :destroy
end
class OrderOption < ApplicationRecord
belongs_to :option
belongs_to :order
end
class Option < ApplicationRecord
belongs_to :product_type
has_many :order_options, dependent: :destroy
end
class ProductType < ApplicationRecord
has_many :options, dependent: :destroy
end
orders_controller
class OrdersController < ApplicationController
def new
#shop = Shop.find(params[:shop_id])
#order = Order.new
#orders = #shop.orders
#order.order_products.build
#order.order_options.build
#product_type_list = #shop.product_types
#order.build_order_contact
#products = []
#options = []
# Display products/options for type
if params[:product_type].present?
#products = ProductType.find(params[:product_type]).products
#options = ProductType.find(params[:product_type]).options
end
if request.xhr?
respond_to do |format|
format.json {
render json: {products: #products, options: #options}
}
end
end
order form
<%= simple_form_for [#shop, #order] do |f|%>
<%= f.simple_fields_for :order_products do |order_product| %>
#select product for which options are shown -->
<%= order_product.simple_fields_for :products do |product| %>
<%= product.input :product_type_id, collection: #product_type_list,
input_html:{
value: #product_type_list.object_id,
id: "product_type"
}
%>
<% end %>
#area to display options belonging to the product
chosen above incl. dropdown field where users
can select a quantity. -->
<h4>Options:</h4>
<div id="render-options">
# Place where Javascript and Ajax can be rendered with possible options and dropdown field for quantity
</div>
<%= f.button :submit%>
Javascript/Ajax
<script >
// Grab selected product_type on which options are based -->
$(document).on("change", "#product_type", function(){
var product_type = $(this).val();
$.ajax({
url: "/shops/<%= #shop.id %>/orders/new",
method: "GET",
dataType: "json",
data: {product_type: product_type},
error: function (xhr, status, error) {
console.error('AJAX Error: ' + status + error);
},
success: function (response) {
console.log(response);
// dynamic rendered options -->
var options = response["options"];
$("#render-options").html("");
$("#render-options").append("");
for(var i=0; i< options.length; i++){
$("#render-options").append('<option value="' + options[i]["id"] + '">' + options[i]["name"] + '</option>');
console.log(options[i].orders)
}
}
});
});
</script>
For this example I'm going to go with a classic order form setup as its less confusing (at least to me).
class Order < ApplicationRecord
has_many :line_items, dependent: :destroy
has_many :products, through: :line_items
accepts_nested_attributes_for :line_items
end
class LineItem
belongs_to :order
belongs_to :product
end
class OrdersController
def new
#order = Order.new
# seed the form with 5 empty lines
5.times { #order.line_options.new }
end
def create
#order = Order.new(order_params)
# ...
end
# ...
private
def order_params
params.require(:order).permit( line_items_attributes: [:product_id, :quantity] )
end
end
<%= simple_form_for [#order] do |f| %>
<%= f.simple_fields_for :line_items do |item| %>
<%= f.association :product %>
<%= f.input :quantity %>
<% end %>
<% end %>
This is basically the setup for a "classic" order form which emulates paper. In reality this is not very useful unless your dealing with poor corporate minions that actually have to fill it out from top to bottom.
For an actual web shop the users are actually clicking on forms from GET /products/id that should actually be sending individual POST /orders/:order_id/line_item requests to a nested route to "add items to the cart". The order itself is often created implicitly.
In general the greatest misstake is thinking that you can treat placing an order as one single http call which creates the whole order and processes it. Instead you need to treat it as a set of atomic operations that lead the user to eventually checking out.

Rails: Can I send a GET request to a create path?

I'm trying to append new comments to a list of existing comments using javascript and ajax. I set up my Comments#create to create a new comment and then render its text. But how can I access this text with ajax?
controllers/comments_controller.rb
def new
#comment = Comment.new
#comments = Comment.all
end
def create
#thing = Thing.find(params[:thing_id])
#comment = #thing.comments.create(comment_params)
render text: #comment.text.to_s + "".html_safe
end
My form for a new comment and ajax/javascript attempt:
<%= form_for([#thing, #comment], remote: true) do |f| %>
<%= f.text_area :text, :placeholder => "Explain your rating..." %>
<div id="btn"><%= f.submit "Post", class: "btn", id: "postacomment" %></div>
<script type="text/javascript">
$("#postacomment").click(function() {
$.get( "<%= new_thing_comment_path(:id => #comment.id) %>", function( data ) {
$('#comments_h2').prepend( data );
});
});
</script>
<% end %>
First of all, don't try to bend HTTP methods to fill your needs, follow them instead.
If you want to respond to javascript with rails, that is fairly easy. On your comments controller:
def new
#comment = Comment.new
#comments = Comment.all
end
def create
#thing = Thing.find(params[:thing_id])
#comment = #thing.comments.create(comment_params)
respond_to do |format|
format.html { redirect_to new_comments_path } #this is just a redirection in case JS is disabled
format.js
end
end
As you can see we are now responding to two types of formats, in this case html and js, this forces you to have those corresponding views, or at least for the js version which may look like this:
app/views/comments/create.js.erb:
$('#comments_h2').prepend("<%= j #comment %>");
In the example above I'm assuming you have a partial for rendering a comment, it should look something like:
app/views/comments/_comment.html.erb:
<h2><%= comment.content %></h2>
Obviously you have to update that file to meet your needs.
Hope it helps!

Adding Forms Dynamically From Dropdown Menu Selection

I have a Ruby on Rails question about adding a dynamic form through a drop down selection rather than having individual buttons for each possible selection.
Right now, through the help of following some Railscasts, my application works where I have three individual buttons that are able to dynamically add three different types of nested forms to the parent, all corresponding to different models with different form partials.
The parent model here is a Workout, allowing traditional_blocks, emon_blocks, and tempo_blocks to be added dynamically using JS.
Workout Model
class Workout < ActiveRecord::Base
has_many :tempos
has_many :traditionals
has_many :emons
accepts_nested_attributes_for :tempos, allow_destroy: true
accepts_nested_attributes_for :emons, allow_destroy: true
accepts_nested_attributes_for :traditionals, allow_destroy: true
end
/app/views/workouts/new.html.erb
<div>
<%= button_to_add_fields "Add EMON Block", f, :emons %>
<%= button_to_add_fields "Add traditional Block", f, :traditionals %>
<%= button_to_add_fields "Add tempo Block", f, :tempos %>
</div>
/apps/helpers/application_helper.rb
module ApplicationHelper
def button_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
button_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
end
app/assets/javascripts/workouts.js.coffee
$(document).on 'click', 'form .add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()
Like I said earlier, everything works as I want it to when I click the buttons. However, I would like to be able to put "emon block, traditional block, tempo block" inside a collection_select with one button next to the collection_select that says "create." When that "create" button is clicked, I would like it to call that same helper(button_to_add_fields) passing along the necessary parameters for it to work the same way it does now with the multiple button implementation but using the currently selected association in the collection select.
Any tips?
You'll want to use Ajax for this
The Railscast you viewed, although helpful, is somewhat limited in the way that it will only allow you to add a single nested form each time
--
Ajax
#config/routes.rb
resources :workouts do
get "ajax_fields/:type", on: :collection
end
#app/models/workout.rb
Class Workout < ActiveRecord::Base
...
def self.build type
workout = self.new
if type
workout.send(type).build
else
workout.tempos.build
workout.traditionals.build
workout.emons.build
end
end
end
#app/controllers/workouts_controller.rb
Class WorkoutsController < ApplicationController
def ajax_update
#workout = Workout.build params[:type]
render "form", layout: !request.xhr?
end
end
#app/views/workouts/_form.html.erb
<%= form_for #workout do |f| %>
<%= render "fields", locals: { f: f }, onject: params[:type] %>
<%= f.submit %>
<% end %>
#app/views/workouts/_fields.html.erb
<%= f.fields_for type.to_sym, child_index: Time.now.to_i do |t| %>
<%= t.text_field :your_field %>
<% end %>
#app/views/workouts/new.html.erb
<%= form_for #workout do |f| %>
<%= render: "fields", locals: { f: f}, collection: ["tempos", "traditionals", "emons"], as: :type %>
<%= ... dropdown code ...%>
<%= f.submit %>
<% end %>
--
This will allow you to send the following ajax request:
#app/assets/javascripts/workouts.js.coffee
$(document).on 'change', 'form .add_fields', (event) ->
type = $(this).val
$.ajax
url: "/workouts/ajax_fields/" + type,
success: function(data) {
$("form").append(data); // will have to work this out properly
}
});
This should give you the ability to append the extra fields you need to the application, which will then come back with the appropriate HTML for you to append to your DOM
Hopefully you can appreciate the sentiment here - it might not work right out of the box!

Creating Rails 4 Form with Join Table Metadata

Very new Rails 4 developer here. I've got a form where a user is creating Exercises. Exercises can have many Equipment, and Equipment can be optional( think push-up stands for doing push-ups ). I store this "optional" field on the join table exercise_equipment.
I cannot get the parameters to actually send through the values of the collection element that I pick. See below for the model, view, controller, and parameters.
Here are the attributes/relationships of my models:
# id :integer
# name :string
# is_public :boolean
Exercise
has_many :exercise_equipment
has_many :equipment, :through => :exercise_equipment
accepts_nested_attributes_for :exercise_equipment
# id :integer
# exercise_id :integer
# equipment_id :integer
# optional :boolean
ExerciseEquipment
belongs_to :exercise
belongs_to :equipment
accepts_nested_attributes_for :equipment
# id :integer
# name :string
Equipment
has_many :exercise_equipment
has_many :exercises, :through => :exercise_equipment
Here are some (maybe) relevant controller methods:
def new
#exercise = Exercise.new
#exercise.exercise_equipment.build
end
def create
#exercise = Exercise.new( exercise_params )
if #exercise.save
redirect_to #exercises
else
render 'new'
end
end
def edit
#exercise = Exercise.find( params[:id] )
end
def update
#exercise = Exercise.find( params[:id] )
if #exercise.update_attributes( exercise_params )
redirect_to #exercises
else
render 'edit'
end
end
def exercise_params
params.require( :exercise ).permit(
:name,
:is_public,
exercise_equipment_attributes: [
:id,
:optional,
equipment_attributes: [
:id,
:name
],
]
)
end
This is my shot at creating a view to do what I want:
exercises/new.html.erb
<%= form_for #exercise do |f| %>
<%= render 'form', f: f %>
<%= f.submit "New Exercise" %>
<% end %>
exercises/_form.html.erb
<%= f.label :name %><br />
<%= f.text_field :name %>
<%= f.check_box :is_public %> Public
<%= f.fields_for( :exercise_equipment ) do |eef|
<%= eef.fields_for( :equipment ) do |ef|
ef.collection_select :id, Equipment.all, :id, :name %>
<% end %>
<%= eef.check_box :is_optional %> Optional
<% end %>
When I put all of this together and submit an update to an already-existing exercise, the values all go through the params hash, but aren't changed to the new values I've selected...
Parameters: {
"utf8"=>"[checkbox]",
"authenticity_token"=>"[token]",
"exercise"=>{
"name"=>"Test",
"is_public"=>"1",
"exercise_equipment_attributes"=>{
"0"=>{
"equipment_attributes"=>{
"id"=>"1"
},
"optional"=>"1",
"id"=>"2"
}
}
},
"commit"=>"Save Exercise",
"id"=>"1"
}
If you can help me out, I'd be super appreciative. Just let me know if you need any more information and I can provide it.
EDIT
Here is the state of the database before updating:
postgres#=>db=# select id, name, is_public from exercises;
id | name | is_public
----+------+-----------
2 | Test | t
(1 row)
Time: 61.279 ms
postgres#=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
id | exercise_id | equipment_id | optional
----+-------------+--------------+----------
2 | 2 | 1 | t
(1 row)
Time: 58.819 ms
postgres#=>db=# select id, name from equipment where id = 1;
id | name
----+-------------
1 | Freeweights
(1 row)
I then go to the update route for that exercise, select a different equipment from the collection, and submit the form. I get the following Rails Console results:
Started PATCH "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:18 -0400
Processing by ExercisesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"PsbbUPSCiIew2Fd22Swn+K4PmLjwNDCrDdwXf9YBcm8=", "exercise"=>{"name"=>"Test", "is_public"=>"1", "exercise_equipment_attributes"=>{"0"=>{"equipment_attributes"=>{"id"=>"1"}, "optional"=>"1", "id"=>"2"}}}, "commit"=>"Save Exercise", "id"=>"system-test"}
Exercise Load (60.5ms) SELECT "exercises".* FROM "exercises" WHERE "exercises"."slug" = 'system-test' ORDER BY "exercises"."id" ASC LIMIT 1
(57.3ms) BEGIN
ExerciseEquipment Load (76.2ms) SELECT "exercise_equipment".* FROM "exercise_equipment" WHERE "exercise_equipment"."exercise_id" = $1 AND "exercise_equipment"."id" IN (2) [["exercise_id", 2]]
Equipment Load (59.1ms) SELECT "equipment".* FROM "equipment" WHERE "equipment"."id" = $1 LIMIT 1 [["id", 1]]
User Load (60.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 10]]
Exercise Exists (60.5ms) SELECT 1 AS one FROM "exercises" WHERE ("exercises"."name" = 'Test' AND "exercises"."id" != 2 AND "exercises"."user_id" = 10) LIMIT 1
(64.8ms) COMMIT
Redirected to http://localhost:3000/exercises/system-test
Completed 302 Found in 590ms (ActiveRecord: 580.0ms)
Started GET "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:19 -0400
Processing by ExercisesController#show as HTML
Parameters: {"id"=>"system-test"}
Exercise Load (64.1ms) SELECT "exercises".* FROM "exercises" WHERE "exercises"."slug" = 'system-test' ORDER BY "exercises"."id" ASC LIMIT 1
Equipment Load (58.7ms) SELECT "equipment".* FROM "equipment" INNER JOIN "exercise_equipment" ON "equipment"."id" = "exercise_equipment"."equipment_id" WHERE "exercise_equipment"."exercise_id" = $1 [["exercise_id", 2]]
Rendered exercises/show.html.erb within layouts/application (122.7ms)
User Load (60.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = 10 ORDER BY "users"."id" ASC LIMIT 1
Rendered shared/_header.html.erb (61.9ms)
Rendered shared/_alerts.html.erb (0.1ms)
Completed 200 OK in 264ms (Views: 21.3ms | ActiveRecord: 240.8ms)
Firstly, you need to make sure you define your associations correctly.
Any has_many association should be defined with a plural name -
#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
has_many :exercise_equipments
has_many :equipments, :through => :exercise_equipments
accepts_nested_attributes_for :exercise_equipments
end
#app/models/exercise_equipment.rb
Class ExerciseEquipment < ActiveRecord::Base
belongs_to :exercise
belongs_to :equipment
end
#app/models/equipment.rb
Class Equipment < ActiveRecord::Base
has_many :exercise_equipments
has_many :exercises, through: :exercise_equipments
end
If you've already got it working, and are happy with what you've got, then I'd recommend keeping your current setup. However, you may wish to adopt the above for convention's sake
Edit I see from the deleted answer that Beartech investigated this, and turns out Rails treats Equipment / Equipments as the same. Will be worth ignoring the above, but I'll leave it for future reference
Params
I cannot get the parameters to actually send through the values of the
collection element that I pick. See below for the model, view,
controller, and parameters.
I think I get what you mean - you're looking to update the record, but it does not send through the updated parameters to your controller, hence preventing it from being updated.
Although I can't see any glaring problems, I would recommend the issue is that you're trying to populate the exercise_id of an Exercise object. You need to define it for the exercise_equipment object:
<%= f.fields_for :exercise_equipment do |eef| %>
<%= eef.collection_select :equipment_id, Equipment.all, :id, :name %>
<%= eef.check_box :is_optional %>
<% end %>
This will populate your exercise_equipment table as described here:
Time: 61.279 ms
postgres#=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
id | exercise_id | equipment_id | optional
----+-------------+--------------+----------
2 | 2 | 1 | t
(1 row)
Currently, you're populating the Equipment model with equipment_id - which won't work. Populating the model in that way will server to create a new record, not update the ones already created
Extra Field
I want to have a link to add an additional equipment field when it is
clicked, similar to how Ryan Bates did it in this RailsCast, but the
helper method he writes( see "Show Notes" tab if you're not subscribed
to see the source ) seems to become substantially more complex when
dealing with the nested views shown in my code below. Any help in
dealing with this?
This a trickier mountain to overcome
Ryan uses quite an outdated method in this process (to pre-populate the link and then just let JS append the field). The "right" way is to build a new object & append the fields_for from ajax. Sounds tough? That's because it is :)
Here's how you do it:
#config/routes.rb
resources :exercises do
collection do
get :ajax_update #-> domain.com/exercises/ajax_update
end
end
#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
def self.build
exercise = self.new
exercise.exercise_equipment.build
end
end
#app/controllers/exercises_controller.rb
Class ExercisesController < ApplicationController
def new
#exercise = Exercise.build
end
def ajax_update
#exercise = Exercise.build
render "add_exercise", layout: false #> renders form with fields_for
end
end
#app/views/exercises/add_exercise.html.erb
<%= form_for #exercise do |f| %>
<%= render partial: "fields_for", locals: { form: f } %>
<% end %>
#app/views/exercises/_fields_for.html.erb
<%= f.fields_for :exercise_equipment, child_index: Time.now.to_i do |eef| %>
<%= eef.collection_select :equipment_id, Equipment.all, :id, :name %>
<%= eef.check_box :is_optional %>
<% end %>
#app/views/exercises/edit.html.erb
<%= form_for #exercise do |f| %>
<%= render partial: "fields_for", locals: { form: f } %>
<%= link_to "Add Field", "#", id: "add_field" %>
<% end %>
#app/assets/javascripts/application.js
$(document).on("click", "#add_field", function() {
$.ajax({
url: "exercises/ajax_update",
success: function(data) {
el_to_add = $(data).html()
$('#your_id').append(el_to_add)
}
});
});

Categories

Resources