You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

1. Model Setup & Associations

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!

2. Controller Setup

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
3. Building the Form

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 %>
4. Dynamic Subject Loading

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
5. Extra JS for Adding/Removing Grades

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 07:42:00