Rails长表单优化:新增控制器能否提升组织性?
Hey Owen, great question! Let's break this down clearly—yes, adding a second controller can absolutely help organize your long health questionnaire, but we'll also tackle your two immediate issues along the way.
Absolutely. When you're dealing with 50+ fields split into distinct sections (like basic health, medication history, lifestyle habits), splitting your single QuestionnaireController into smaller, focused controllers (e.g., BasicHealthController, MedicationHistoryController) will:
- Keep each controller's code concise and focused on one section of the form
- Let you split the long form into a multi-step/wizard flow (better for user experience)
- Make it easier to maintain and update individual sections without touching the entire codebase
That said, this works best when paired with model refactoring—don't cram all 50 fields into a single Questionnaire model. Extract repeatable sections (like medication history) into their own associated models (e.g., Medication).
(1) Friendlier field names in the show view
You don't need a new controller to fix this—there are two straightforward approaches:
- Add human-readable attribute names directly in your
Questionnairemodel:
Now SimpleForm will automatically use these friendly names in both forms and theclass Questionnaire < ApplicationRecord HUMANIZED_NAMES = { q1: "Have you had a temperature over 37.5°C in the last week?", q2: "Do you have a history of chronic heart disease?", # Add all your field mappings here } def self.human_attribute_name(attr, options = {}) HUMANIZED_NAMES[attr.to_sym] || super endshowview, no manual edits needed. - If you prefer to set labels per-view, override them directly in your SimpleForm calls:
<%= f.input :q1, label: "Have you had a temperature over 37.5°C in the last week?" %>
(2) Dynamic multi-row fields for repeatable sections
This requires nested attributes + a bit of JavaScript, and splitting into an associated model will make this cleaner:
- Extract the repeatable section to a new model:
# app/models/questionnaire.rb class Questionnaire < ApplicationRecord has_many :medications, dependent: :destroy accepts_nested_attributes_for :medications, allow_destroy: true, reject_if: :all_blank end # app/models/medication.rb class Medication < ApplicationRecord belongs_to :questionnaire end - Build the dynamic form with SimpleForm:
In your main questionnaire form:<%= simple_form_for @questionnaire do |f| %> <!-- Your non-repeatable health fields here --> <h3>Medication History</h3> <table class="medication-table"> <thead> <tr> <th>Medication Name</th> <th>Dosage</th> <th>Duration of Use</th> <th></th> </tr> </thead> <tbody id="medications-container"> <%= f.simple_fields_for :medications do |med_form| %> <%= render "medication_fields", f: med_form %> <% end %> </tbody> </table> <%= link_to "Add Medication", "#", id: "add-med-btn", class: "btn" %> <%= f.submit "Save Questionnaire" %> <% end %> - Create a partial for medication fields:
app/views/questionnaires/_medication_fields.html.erb:<tr class="medication-row"> <td><%= f.input :name, label: false %></td> <td><%= f.input :dosage, label: false %></td> <td><%= f.input :duration, label: false %></td> <td> <%= f.hidden_field :_destroy %> <%= link_to "Remove", "#", class: "remove-med-btn" %> </td> </tr> - Add JavaScript for dynamic rows:
document.addEventListener('DOMContentLoaded', function() { const addBtn = document.getElementById('add-med-btn'); const container = document.getElementById('medications-container'); const template = document.querySelector('.medication-row').cloneNode(true); addBtn.addEventListener('click', function(e) { e.preventDefault(); const timestamp = new Date().getTime(); // Replace placeholder IDs with a unique timestamp const newFields = template.innerHTML.replace(/\[(\d+)\]/g, `[${timestamp}]`).replace(/_(\d+)_/g, `_${timestamp}_`); const newRow = document.createElement('tr'); newRow.innerHTML = newFields; container.appendChild(newRow); }); // Handle row removal document.addEventListener('click', function(e) { if (e.target.classList.contains('remove-med-btn')) { e.preventDefault(); const row = e.target.closest('.medication-row'); row.querySelector('input[type="hidden"]').value = '1'; row.style.display = 'none'; } }); });
If you go the controller split route, organize controllers by form section:
BasicHealthController: Handles static, non-repeatable fields (age, gender, general health)MedicationHistoryController: Manages CRUD for medication entries (can work with nested routes underquestionnaires)LifestyleController: Handles lifestyle-related questions (smoking, exercise, diet)
This way, each controller only manages one logical section of your questionnaire, making debugging and updates far easier.
内容的提问来源于stack exchange,提问作者Owen

