Making a conditionnal form using Rails & Stimulus - ruby-on-rails

I'm a newbie in computer development, I have started using Ruby on Rails framework with Stimulus to add some dynamic elements to my pages.
Context
I am working on a video game recommendation system, to do so the player will have to answer a detailled list of questions.
To be as pertinent as possible, I would like to make some conditional questions.
Example : You answer "B" to question 1, the next question is 1-B
You answer "A" to question 1, the next question is 1-A, and so on.
How to do it
After some research, it seems that the best practice to do so would be to have a massive form with every input and to show/hide the ones depending of the values.
Player gives input B to the question 1
I check the value of the question 1 input
If the value is A, I show the question 1-A
If the value is B ...
The problem
So far, I managed to get to the point 3. but with some issues.
When I'm using the simple_form :collection attributes which gives a drop-down menu, I manage to access the input value in the list using the Stimulus target "input".
The not-so working version with a collection:
However, if I change the input type to "radio_boxes" I can only access the first value in the collection.
The not working version with a radio_buttons type for the input
I guess my problem is located in the JS controller, would be awesome if you had any idea regarding this issue!
My code
I have two tables, a player and a personnality which belongs to a player :
Player model:
class Player < ApplicationRecord
belongs_to :user
validates :user, uniqueness: true
has_one :personality, dependent: :destroy
accepts_nested_attributes_for :personality
end
Personnality model:
class Personality < ApplicationRecord
belongs_to :player
validates :player, uniqueness: true
SATURDAY_NIGHT_QUESTION = "It's Saturday night, you are probably"
SATURDAY_NIGHT_ANSWERS = [
"Staying at home",
"Going out"
]
SATURDAY_HOME_QUESTION = "Alright, comfy Saturday at home sounds good!, what do you do there?"
SATURDAY_OUT_QUESTION = "Alright, party time then! Where are we headed to?"
SATURDAY_HOME_ANSWERS = [
"Reading a book",
"Playing online with some friends"
]
SATURDAY_OUT_ANSWERS = [
"Going to a bar of course",
"A friend is having a boardgame session"
]
end
I have put my questions & answers in my models to centralize it, in case I will have to edit it afterwards (is it a good idea by the way to put it in the model?).
View
<p>Welcome, player</p>
<div data-controller="sample-dropdown">
<%= simple_form_for(#player) do |f| %>
<%= f.simple_fields_for :personality do |p| %>
<% p.input :player_id, as: :hidden, input_html: { id: p.object.player_id }%>
<%= p.input :saturday_night, label: Personality::SATURDAY_NIGHT_QUESTION,
collection: Personality::SATURDAY_NIGHT_ANSWERS, as: :radio_buttons,
input_html: {data:{target: 'sample-dropdown.input', action:'input->sample-dropdown#answer'}} %>
<div class="disable" data-sample-dropdown-target="test1">
<%= p.input :saturday_action, label: Personality::SATURDAY_HOME_QUESTION, collection: Personality::SATURDAY_HOME_ANSWERS, include_blank: false %>
</div>
<div class="disable" data-sample-dropdown-target="test2">
<%= p.input :saturday_action, label: Personality::SATURDAY_OUT_QUESTION, collection: Personality::SATURDAY_OUT_ANSWERS, include_blank: false %>
</div>
<% end %>
<%= f.button :submit %>
<% end %>
<% console %>
</div>
I am using the simple form gem with Stimulus a small JS framework.
Here is my JS Controller :
mport { Controller } from "#hotwired/stimulus"
// Connects to data-controller="sample-dropdown"
export default class extends Controller {
static targets = ["input", "test1", "test2"]
connect() {
console.log(this.inputTarget)
}
answer() {
const element = this.inputTarget
console.log(element.value)
switch (element.value) {
case "Staying at home":
this.test1Target.classList.remove("disable")
this.test1Target.classList.add("active")
this.test2Target.classList.remove("active")
this.test2Target.classList.add("disable")
case "Going out":
this.test2Target.classList.remove("disable")
this.test2Target.classList.add("active")
this.test1Target.classList.remove("active")
this.test1Target.classList.add("disable")
}
}
}

Related

Can't convert symbol to integer error with rails simple_form nested attributes

I've seen many similar questions, and looked at the answers, but nothing seems to be helping me and I've been working on this for a while now. The error is 'Can't convert symbol into integer'. My goal is to be able to create multiple sub_groups for each race. I'm just starting with trying to create one for the time being. Here's the relevant code...
** UPDATE **
VIEW
<%= simple_form_for(#race, :url => form_path, :method => form_method, :html => { :class =>
'form-horizontal form-compressed' }) do |f| %>
<fieldset>
<%= f.simple_fields_for :sub_groups do |g| %>
<%= g.input :name, requred: false %>
<%= g.collection_radio_buttons :discount_type,
[['dollars', '$'], ['percent', '%']], :first, :last %>
<%= g.input :discount_amount, :as => :integer, required: false %>
<% end %>
<hr/>
** RACE MODEL**
class Race < ActiveRecord::Base
has_many :sub_groups
accepts_nested_attributes_for :sub_groups
attr_accessible :sub_groups_attributes
** SUB_GROUP MODEL **
class SubGroup < ActiveRecord::Base
belongs_to :race
has_many :race_users
attr_accessible :discount_amount, :discount_type, :display_results, :name
end
PARAMS
after my code update...
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"VihBL4TDT/Lte4YBji/4fp4XvOri1UgUZ8B33wQuCko=", "race"=>
{"sub_group"=>{"name"=>"dfd", "discount_type"=>"dollars", "discount_amount"=>"2"}},
"commit"=>"Next", "wizard"=>"2", "id"=>"13-test5"}
CONTROLLER
class RacesController < ApplicationController
def new
#race = Race.new
#sub_groups = #race.sub_groups.build
#wizard_step = -1
#wizard_step_name = Race.wizard_step_name_from_id #wizard_step
#wizard_mode = true
render :layout => "race_wizard"
end
def update
#race = Race.find params[:id]
#wizard_step = params[:wizard].to_i + 1
#race.wizard_step = #wizard_step
#race.update_attributes(params[:race])
end
So I took advice from answer 1, and switched to using :sub_groups in the view. Now I have a new problem, which is the sub-group fields don't show up at all, despite the fact that I built a sub_groups thing in the #new method.
I'm really stumped on how I can make this happen. This is driving me bonkers. Any help is much appreciated. Thanks!
The way fields_for works is that if you supply a symbol it checks whether your model respond to {given_symbol}_attributes=. If it does the name of sub-fields is {given symbol}_attributes, otherwise just {given_symbol}.
What you need is to add accepts_nested_attributes_for :sub_groups to your Race model. This methods will create a default setter sub_groups_attributes=, which will make fields_for :sub_groups to generate fields with name sub_groups_attributes.
You can also write your own sub_groups_attributes= method, but you need to be sure you know what you're doing there as it might be a little tricky to debug.
Note, that fields_to :sub_groups won't display fields if there are no sub_group associated with given object - you will need to build one in your controller first.

How to set the "selected" choice in the selection box for an associated model?

I have a Product model with many Versions:
class Product < ActiveRecord::Base
attr_accessible :name, :versions_attributes
has_many :versions
accepts_nested_attributes_for :versions, allow_destroy: true
end
class Version < ActiveRecord::Base
attr_accessible :available_q, :kind, :product_id
belongs_to :product
end
I would like to present the available_q attribute to the (admin) user as a select box with the choices of "Yes" or "No", and of course I would like the to have the select box show whatever is currently in the version database for this version, but can't seem to get it to do that. Here is the portion of the view code for the product form involving the select box for the associated model:
<%= form_for(#product) do |f| %>
…
<%= f.fields_for :versions do |version| %>
<%= version.select :available_q, options_for_select([['Yes', 't'],['No', 'f']], version.object.available_q) %><br />
…
<% end %>
…
Everything works well except that the current select box always shows yes even after updating the database with a 'No'. It's likely that I have forgotten to do something very simple, but would very much appreciate any help on this.
<%= version.select :available_q, options_for_select([['Yes', 't'],['No', 'f']], version.object.available_q == 't' ? 0 : 1) %>
You can try the solution above. The second parameter of options_for_select isn't the value to be shown, but the index of the value on the collection array [['Yes', 't'],['No', 'f']].
I have found a work-around: the difficulty I was having seems to have to do with using :available_q which has boolean data type. When I change it to string type, the problem goes away!

Move Multiple-Input Virtual Attributes to SimpleForm Custom Input Component

Height is stored in the database in inches.
However feet and inches need their own individual inputs in the form:
Height: [_______] feet [_______] inches
So I used virtual attributes, and got it working. Here is a simplified version of my model:
class Client < ActiveRecord::Base
attr_accessible :name, :height_feet, :height_inches
before_save :combine_height
def height_feet
height.floor/12 if height
end
def height_feet=(feet)
#feet = feet
end
def height_inches
if height && height%12 != 0
height%12
end
end
def height_inches=(inches) #on save?
#inches = inches
end
def combine_height
self.height = #feet.to_d*12 + #inches.to_d #if #feet.present?
end
end
And the _form partial using simple_form:
<%= simple_form_for(#client) do |f| %>
<ul>
<%= f.error_notification %>
<%= f.input :name %>
<%= f.input :weight %>
<li>
<%= f.input :height_feet, :label => 'Height', :wrapper => false %>
<span>feet</span>
<%= f.input :height_inches, :label => false, :wrapper => false %>
<span>inches</span>
</li>
<%= f.error :base %>
</ul>
<%= f.button :submit %>
<% end %>
This works. But it is not ideal.
I'd like to DRY this up and create a custom input component so I can add height to the form with <%= f.input :height, as: :feet_and_inch %>—and therefore any other input that follows the same pattern such as <%= f.input :wingspan, as: :feet_and_inch %>.
I've experimented with custom components, but I can't get two inputs to display—and I'm not sure where is the best place to put the 'conversion' logic from feet and inches to inches (and likewise from inches back to feet and inches).
As far as I know, you can't really move anything but the rendering to custom input. SimpleForm doesn't get called once the form is submitted so it can't really interfere with the values in any way. I would love to be wrong about this as I needed it in the past also. Anyway, here's a version that keeps the conversion logic in the model.
The custom SimpleForm input:
# app/inputs/feet_and_inch_input.rb
class FeetAndInchInput < SimpleForm::Inputs::Base
def input
output = ""
label = #options.fetch(:label) { #attribute_name.to_s.capitalize }
feet_attribute = "#{attribute_name}_feet".to_sym
inches_attribute = "#{attribute_name}_inches".to_sym
output << #builder.input(feet_attribute, wrapper: false, label: label)
output << template.content_tag(:span, " feet ")
output << #builder.input(inches_attribute, wrapper: false, label: false)
output << template.content_tag(:span, " inches ")
output.html_safe
end
def label
""
end
end
The form. Note that I did not put the <li> tags inside the custom input, I think this way it's more flexible but feel free to change it.
# app/views/clients/_form.html.erb
<li>
<%= f.input :height, as: :feet_and_inch %>
</li>
All of this relies on the fact that for every height attribute, you also have height_feet and height_inches attributes.
Now for the model, I am not honestly sure if this is the way to go, maybe someone might come up a better solution, BUT here it goes:
class Client < ActiveRecord::Base
attr_accessible :name
["height", "weight"].each do |attribute|
attr_accessible "#{attribute}_feet".to_sym
attr_accessible "#{attribute}_inches".to_sym
before_save do
feet = instance_variable_get("##{attribute}_feet_ins_var").to_d
inches = instance_variable_get("##{attribute}_inches_ins_var").to_d
self.send("#{attribute}=", feet*12 + inches)
end
define_method "#{attribute}_feet" do
value = self.send(attribute)
value.floor / 12 if value
end
define_method "#{attribute}_feet=" do |feet|
instance_variable_set("##{attribute}_feet_ins_var", feet)
end
define_method "#{attribute}_inches=" do |inches|
instance_variable_set("##{attribute}_inches_ins_var", inches)
end
define_method "#{attribute}_inches" do
value = self.send(attribute)
value % 12 if value && value % 12 != 0
end
end
end
It basically does the same but defines the methods dynamically. You can see at the top there's a list of attributes for which you want these methods to be generated.
Note that all of this is not really thoroughly tested and might kill your cat but hopefully can give you some ideas.
My humble opinion is that you would give better user experience if the user inputs the data in just one field . Here are my concerns :
Assuming you are using heights in limited range (probably human's height) , you can write a validation that detects what is the user input - inches or feet . Then you could make a validation link (or better a button ) asking if the input is what it meant to be (inches or feet detected) .
All this (including the dimension transformation while it's just inches -> feet) can be done in javascript , you can fetch the current dimensions by Ajax call and avoid reloading the whole code of the page .
EDIT : I've found an interesting point of view related with complicated inputs . Another useful resource about user interaction in filling form with feet and inches .
Your question is really interesting and I would love to see the solution you choose .

RecordNotFound with accepts_nested_attributes_for and belongs_to

I get
ActiveRecord::RecordNotFound: Couldn't find Client with ID=3 for Order with ID=
when trying to submit an Order form for an existing client. This happens through the form or the console by typing:
Order.new(:client_attributes => { :id => 3 })
payment_form.html.erb:
<%= semantic_form_for #order, :url => checkout_purchase_url(:secure => true) do |f| %>
<%= f.inputs "Personal Information" do %>
<%= f.semantic_fields_for :client do |ff| %>
<%= ff.input :first_name %>
<%= ff.input :last_name %>
<!-- looks like semantic_fields_for auto-inserts a hidden field for client ID -->
<% end %>
<% end %>
<% end %>
Order.rb:
class Order < ActiveRecord::Base
belongs_to :client
accepts_nested_attributes_for :client, :reject_if => :check_client
def check_client(client_attr)
if _client = Client.find(client_attr['id'])
self.client = _client
return true
else
return false
end
end
end
The reject_if idea came from here but I logged the method and it's not even being called! It doesn't matter what its name is!
Note: Feb 2020
Since I'm starting to get downvotes on this 8 years later, adding this note. While this was the original solution I went with 8 years ago, a better one has been proposed by MatayoshiMariano (5 years after my OP).
My Original Fix
Fixed the issue by overloading the client_attributes= method, as described here:
def client_attributes=(client_attrs)
self.client = Client.find_or_initialize_by_id(client_attrs.delete(:id))
self.client.attributes = client_attrs
end
If you only want a new Order with an existing client, without modifying the client, you need to assign the id.
Order.new(client_id: 3)
This is another way to do this without overloading the client_attributes= method and cleanest
The new Order now has the client with ID 3
If you also want to update ant client's attributes you must add the client_attributes, for example:
Order.new(client_id: 3, client_attributes: { id: 3, last_order_at: Time.current })
See https://github.com/rails/rails/issues/7256 from 2012.
If you have has_many relationship, this will work. Tested on Rails 6.0.2
def clients_attributes =(attributes)
# Get IDs for any clients that already exist.
client_ids = attributes.values.map { |a| a[:id] }.compact
# Now find them all and move them to this section.
clients << Client.find(client_ids)
# Update them with standard `accepts_nested_attributes_for` behaviour.
super attributes
end
Had the same error creating a new Thing for existing model with has_many and belongs_to relations.
Fixed it by adding a hidden field for the id of the existing model, for instance User, to the form.
= form.input :user_id, as: :hidden
Then new Thing was created without the error.

Modifying attributes on the join model with accepts_nested_attributes_for

Simply, a Contact can have various associated Time Windows, which may or may not be Active as a Schedule. To wit:
Models
class Contact < ActiveRecord::Base
has_many :schedules
has_many :time_windows, :through => :schedules
accepts_nested_attributes_for :schedules, :allow_destroy => true
end
class TimeWindow < ActiveRecord::Base
has_many :schedules
has_many :contacts, :through => :schedules
end
class Schedule < ActiveRecord::Base
belongs_to :contact
belongs_to :time_window
end
View
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<p>
<%= f.label tw.description %>
<%= hidden_field_tag "contact[schedules_attributes][][id]", schedule.id %>
<%= check_box_tag "contact[schedules_attributes][][time_window_id]",
tw.id, #contact.time_windows.include?(tw) %>
<%= check_box_tag "contact[schedules_attributes][][active]", nil,
schedule.active %>
</p>
<% end %>
This submits something like this:
Parameters: { "commit" => "Update", "contact" => {
"group_ids" => ["2"], "enabled" => "1",
"schedules_attributes" => [ { "time_window_id"=>"1", "id"=>"46"},
{ "time_window_id" => "2", "id" => "42", "active" => "on" },
{ "time_window_id" => "3", "id" => "43"},
{ "time_window_id" => "4", "id" => "44", "active" => "on"}],
"last_name" => ...
The update action in the controller is basically stock, except to handle another instance of another related model which I coded using the "Handling Multiple Models" example from the Advanced Rails Recipes book.
According to this API doc, I think the above ought to work. However, nothing about the Schedules is getting updated. This shows up in the server log:
[4;35;1mSchedule Update (0.2ms)[0m [0mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 42[0m
[4;36;1mSchedule Update (0.1ms)[0m [0;1mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 44[0m
(NetBeans is giving me those stupid "[0m"'s in the output. I don't know what's wrong there.)
The SQL shows that the "active" boolean field is getting set to 0 where checked. How do I get this to correctly set the active bit?
As a followup, how would I organize this to get rid of the Schedule "connection" at all? I'm thinking I need to submit a :_delete with the Schedule from the form, but how would I do that conditionally when a checkbox is involved?
Thanks for any help you can provide. Rails is turning out to be a vast subject for me, and I want to do it "right." I'm really close here, but there's got to be a way to make this -- not just correct -- but elegant. The view code just feels way too cumbersome to be proper Rails. ;-)
I've kept trying different approaches to this problem, and I've come up with this, which works. Mostly. The only problem is that it doesn't handle NOT having a "Schedule" for each "Time Window". The form will render, and I'll get a disabled check_box (to prevent me from trying to delete something that isn't there), but I don't have a way to add it back, and submitting without it throws off the params hash (and causes Rails to give me an "Expected Hash (got Array)" error)
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<% f.fields_for "schedules_attributes[]", schedule do |sf| %>
<p>
<%= sf.label tw.description %>
<%= sf.hidden_field :id %>
<%= sf.check_box :_destroy, :disabled => schedule.new_record? %>
<%= sf.check_box :active %>
</p>
<% end %>
<% end %>
Note that the "schedules_attributes[]" array will automatically give you an existing ID within the braces in your HTML (which is nice), but the _attributes hash is expecting an "id" alongside the other attributes in order to make sense of the sub-hashes.
One of the big lessons I've learned here is that the "check_box_tag" method doesn't (seem to) give me a paired-up hidden field for Rails to parse in the unchecked case. I would have expected this. Adding one in by hand made a mess, which led me to finally giving into the "fields_for" method, and trying many incarnations before finding the appropriate syntax to get what I wanted out of it.
I've realized that my model isn't quite appropriate in this setup, so I'm going to change it, but I was so close to this answer, I wanted to at least get to the point of being able to see the end before I moved on.

Resources