Rails多嵌套表单(含默认/预设值)重建需求咨询
Got it, let's walk through building this complex nested form in Rails. It's a bit layered, but breaking it down step by step will make it manageable. Here's how I'd approach it:
First, we need to define all models and their relationships to match your requirements. Let's start with the core models:
Student Model
# app/models/student.rb class Student < ApplicationRecord belongs_to :school has_many :student_grades, dependent: :destroy has_many :grades, through: :student_grades accepts_nested_attributes_for :grades, allow_destroy: true end
School Model
# app/models/school.rb class School < ApplicationRecord has_many :students, dependent: :destroy has_many :subjects, dependent: :destroy end
Grade Model (学年)
# app/models/grade.rb class Grade < ApplicationRecord has_many :student_grades, dependent: :destroy has_many :students, through: :student_grades has_many :grade_assessments, dependent: :destroy accepts_nested_attributes_for :grade_assessments, allow_destroy: true end
GradeAssessment Model (评估项)
We'll use an enum to handle the three fixed term types (期初/期中/期末) and add a uniqueness validation to ensure each grade only has one of each term:
# app/models/grade_assessment.rb class GradeAssessment < ApplicationRecord belongs_to :grade has_many :subject_scores, dependent: :destroy enum term_type: { beginning: "期初", midterm: "期中", final: "期末" }, _default: "beginning" validates :term_type, uniqueness: { scope: :grade_id, message: "每个年级的该评估项已存在" } accepts_nested_attributes_for :subject_scores, allow_destroy: true end
Subject Model
# app/models/subject.rb class Subject < ApplicationRecord belongs_to :school has_many :subject_scores, dependent: :destroy end
SubjectScore Model (科目评估得分)
# app/models/subject_score.rb class SubjectScore < ApplicationRecord belongs_to :grade_assessment belongs_to :subject # Add any fields you need for assessment, e.g.: # validates :score, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true end
Don't forget to generate migrations for all these models with appropriate foreign keys!
Next, let's configure the Students controller to handle nested attributes and pre-build necessary records for the form:
# app/controllers/students_controller.rb class StudentsController < ApplicationController def new @student = Student.new # Pre-build one grade for the student @student.grades.build # Pre-build all three assessment terms for the new grade GradeAssessment.term_types.keys.each do |term| assessment = @student.grades.first.grade_assessments.build(term_type: term) # Build subject scores if a school is pre-selected if params[:school_id] School.find(params[:school_id]).subjects.each do |subject| assessment.subject_scores.build(subject: subject) end end end end def create @student = Student.new(student_params) if @student.save redirect_to @student, notice: "学生信息创建成功" else render :new end end def edit @student = Student.find(params[:id]) # Ensure all three assessment terms exist for each grade @student.grades.each do |grade| GradeAssessment.term_types.keys.each do |term| grade.grade_assessments.find_or_create_by(term_type: term) end # Ensure subject scores exist for all school subjects in each assessment grade.grade_assessments.each do |assessment| @student.school.subjects.each do |subject| assessment.subject_scores.find_or_create_by(subject: subject) end end end end private def student_params params.require(:student).permit( :name, :school_id, grades_attributes: [ :id, :name, :_destroy, grade_assessments_attributes: [ :id, :term_type, :_destroy, subject_scores_attributes: [ :id, :subject_id, :score, :_destroy ] ] ] ) end end
Now let's create the nested form using Rails' form_with helper. We'll add dynamic subject loading for when the user selects a school:
Student Form (app/views/students/_form.html.erb)
<%= form_with model: @student do |form| %> <% if @student.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@student.errors.count, "error") %> prohibited this student from being saved:</h2> <ul> <% @student.errors.each do |error| %> <li><%= error.full_message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= form.label :name %> <%= form.text_field :name %> </div> <div class="field"> <%= form.label :school_id %> <%= form.collection_select :school_id, School.all, :id, :name, include_blank: "选择学校", data: { turbo_stream: true, action: "change->students#load_subjects" } %> </div> <!-- Nested Grades --> <%= form.fields_for :grades do |grade_form| %> <h3>学年信息</h3> <%= grade_form.hidden_field :_destroy %> <div class="field"> <%= grade_form.label :name %> <%= grade_form.text_field :name %> <%= link_to "删除学年", "#", class: "remove-grade" %> </div> <!-- Nested Grade Assessments --> <%= grade_form.fields_for :grade_assessments do |assessment_form| %> <h4><%= GradeAssessment.human_enum_name(:term_type, assessment_form.object.term_type) %></h4> <%= assessment_form.hidden_field :term_type %> <%= assessment_form.hidden_field :_destroy %> <!-- Subject Scores (dynamically loaded) --> <div id="subject-scores-<%= assessment_form.object.id %>"> <%= render "subject_scores", form: assessment_form, subjects: @student.school&.subjects || [] %> </div> <% end %> <% end %> <%= link_to "添加学年", "#", class: "add-grade" %> <div class="actions"> <%= form.submit %> </div> <% end %>
Subject Scores Partial (app/views/students/_subject_scores.html.erb)
<% subjects.each do |subject| %> <%= form.fields_for :subject_scores, form.object.subject_scores.find_or_initialize_by(subject: subject) do |score_form| %> <div class="field"> <%= score_form.label subject.name %> <%= score_form.hidden_field :subject_id %> <%= score_form.number_field :score, step: 0.5 %> </div> <% end %> <% end %>
To update subject scores when the user selects a school, add this action to the Students controller:
# app/controllers/students_controller.rb def load_subjects @school = School.find(params[:student][:school_id]) @student = Student.new(student_params) @student.grades.each do |grade| grade.grade_assessments.each do |assessment| assessment.subject_scores.destroy_all @school.subjects.each do |subject| assessment.subject_scores.build(subject: subject) end end end respond_to do |format| format.turbo_stream do @student.grades.each do |grade| grade.grade_assessments.each do |assessment| turbo_stream.update "subject-scores-#{assessment.id}", render("subject_scores", form: form_for(assessment, builder: ActionView::Helpers::FormBuilder) { |f| f }, subjects: @school.subjects) end end end end end
Add the route for this action:
# config/routes.rb resources :students do post :load_subjects, on: :collection end
If you want users to add multiple grades or remove existing ones, add this Stimulus controller:
// app/javascript/controllers/student_form_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { this.addGradeButton = this.element.querySelector(".add-grade") this.addGradeButton.addEventListener("click", (e) => this.addGrade(e)) this.element.querySelectorAll(".remove-grade").forEach(button => { button.addEventListener("click", (e) => this.removeGrade(e)) }) } addGrade(e) { e.preventDefault() const gradesFields = this.element.querySelector("[data-target='students.grades']") const newGrade = gradesFields.dataset.fields.replace(/NEW_RECORD/g, Date.now()) gradesFields.insertAdjacentHTML("beforeend", newGrade) } removeGrade(e) { e.preventDefault() const gradeField = e.target.closest(".fields") gradeField.querySelector("input[name*='_destroy']").value = 1 gradeField.style.display = "none" } }
内容的提问来源于stack exchange,提问作者SeanWM

