# Terminal
bundle add hotwire-rails
rails hotwire:install
rails g scaffold questionnaire name
rails g model question questionnaire:belongs_to name question_type:integer required:boolean
rails g model answer question:belongs_to name
# models/questionnaire.rb
class Questionnaire < ApplicationRecord
has_many :questions, dependent: :destroy
accepts_nested_attributes_for :questions, allow_destroy: true
end
# models/question.rb
class Question < ApplicationRecord
belongs_to :questionnaire
has_many :answers, dependent: :destroy
accepts_nested_attributes_for :answers, allow_destroy: true
enum question_type: { single_choice: 0, multiple_choice: 1, long_answer: 2 }
def self.question_type_select
question_types.keys.map { |k| [k.titleize, k] }
end
end
# models/answer.rb
class Answer < ApplicationRecord
belongs_to :question
end
# app/assets/javascripts/controllers/nested_form_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["add_item", "template"]
add_association(event) {
event.preventDefault()
var content = this.templateTarget.innerHTML.replace(/TEMPLATE_RECORD/g, new Date().getTime())
this.add_itemTarget.insertAdjacentHTML('beforebegin', content)
}
remove_association(event) {
event.preventDefault()
let item = event.target.closest(".nested-fields")
item.querySelector("input[name*='_destroy']").value = 1
item.style.display = 'none'
}
}
# views/questionnaires/_form.html.erb
<%= form_with(model: questionnaire) do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div data-controller="nested-form">
<template data-nested-form-target='template'>
<%= form.fields_for :questions, Question.new, child_index: 'TEMPLATE_RECORD' do |question| %>
<%= render 'question_fields', form: question %>
<% end %>
</template>
<%= form.fields_for :questions do |question| %>
<%= render 'question_fields', form: question %>
<% end %>
<div data-nested-form-target="add_item">
<%= link_to "Add Question", "#", data: { action: "nested-form#add_association" } %>
</div>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
# views/questionnaires/_question_fields.html.erb
<div class='nested-fields box' data-controller='dynamic-select'>
<div class='form-group'>
<%= form.select :question_type,
options_for_select(Question.question_type_select, selected: form.object.question_type),
{},
'data-dynamic-select-target': 'select',
'data-action': 'dynamic-select#selected' %>
</div>
<div class='form-group'>
<%= form.hidden_field :_destroy %>
<%= form.text_field :name, placeholder: 'Question', class: 'form-control' %>
<small>
<%= link_to "Remove", "#", data: { action: "click->nested-form#remove_association" } %>
</small>
</div>
<div data-controller="nested-form" data-dynamic-select-target='choice'>
<template data-nested-form-target='template'>
<%= form.fields_for :answers, Answer.new, child_index: 'TEMPLATE_RECORD' do |answer| %>
<%= render 'answer_fields', form: answer %>
<% end %>
</template>
<%= form.fields_for :answers do |answer| %>
<%= render 'answer_fields', form: answer %>
<% end %>
<div data-nested-form-target="add_item">
<%= link_to "Add Answer", "#", data: { action: "nested-form#add_association" } %>
</div>
</div>
<div data-controller="nested-form" data-dynamic-select-target='long'>
</div>
</div>
# questionnaire_controller.rb
def questionnaire_params
params.require(:questionnaire).permit(
:name,
questions_attributes: [
:_destroy,
:id,
:question_type,
:name,
answers_attributes: [:_destroy, :id, :name]
]
)
end
# views/questionnaires/_answer_fields.html.erb
<div class='nested-fields'>
<div class='form-group'>
<%= form.hidden_field :_destroy %>
<%= form.text_field :name, placeholder: 'Answer', class: 'form-control' %>
<small>
<%= link_to "Remove Answer", "#", data: { action: "click->nested-form#remove_association" } %>
</small>
</div>
</div>
# app/assets/javascripts/controllers/dynamic_select_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["select", "choice", "long"]
connect() {
this.selected()
}
selected() {
this.hideFields()
switch (this.selectTarget.value) {
case 'single_choice':
this.choiceTarget.classList.remove('hidden')
break;
case 'multiple_choice':
this.choiceTarget.classList.remove('hidden')
break;
case 'long_answer':
this.longTarget.classList.remove('hidden')
break;
}
}
hideFields() {
this.choiceTarget.classList.add('hidden')
this.longTarget.classList.add('hidden')
}
}
# views/questionnaires/show.html.erb
<h1><%= @questionnaire.name %></h1>
<% @questionnaire.questions.each do |question| %>
<h2><%= question.name %></h2>
<% case question.question_type %>
<% when 'single_choice' %>
<% question.answers.each do |answer| %>
<p>
<%= radio_button_tag question.id, answer.id %>
<%= answer.name %>
</p>
<% end %>
<% when 'multiple_choice' %>
<% question.answers.each do |answer| %>
<p>
<%= check_box_tag question.id, answer.id %>
<%= answer.name %>
</p>
<% end %>
<% when 'long_answer' %>
<%= text_area_tag question.id %>
<% end %>
<% end %>