通过复选框添加多个嵌套属性 Rails 4(可能有多种形式) [英] Add Multiple Nested Attributes through checkboxes Rails 4 (maybe with multiple forms)

查看:14
本文介绍了通过复选框添加多个嵌套属性 Rails 4(可能有多种形式)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

3/13 更新:
我用我的模型、控制器逻辑和几个表单版本制作了一个小示例项目.



我正在构建一个表单,用户可以在其中添加任务"和里程碑".(即任务 = '真空' 在里程碑 = '干净的房子'内).它基本上是一个任务/子任务类型模型,父级为里程碑",子级为任务".

任务和里程碑都属于项目"....所以我试图通过带有更新操作的嵌套表单添加任务和里程碑.我在想要走的路是为每个 @task_template 实例创建一个表单并一次更新多个表单.

我的问题是,我还试图通过名为MilestoneTemplates"和TaskTemplates"的表动态设置入门里程碑/任务"....

用户打开添加里程碑/任务"页面,根据他们的项目类型,他们会看到一系列预构建的任务(@task_templates)&复选框旁边的里程碑(@milestone_templates).然后,用户选中他们想要添加的任务或里程碑旁边的复选框.这应该使用预先构建的@task_template.name、@task_template.description...等为用户创建一个特定的任务

我什至无法创建 1.我正在使用 Rails 4,我想我已经正确设置了我的 strong_params.下面是我的位置:

型号:

class 任务 

控制器:

class ProjectsController <应用控制器定义新的里程碑@project = Project.find(params[:p])@project.里程碑.build@project.tasks.build@milestones_templates = MilestoneTemplate.where(template_id: @project.template_id)结尾def create_milestones@project.milestone_ids = params[:project][:milestones]@project.task_ids = params[:project][:tasks]@里程碑=里程碑.new@task = Task.new@template = Template.find( @project.template_id)如果@project.update_attributes(project_params)redirect_to view_milestones_path(p:@project.id)flash[:notice] = "成功添加任务和里程碑"别的redirect_to new_milestones_path(p: @project.id )format.json { 渲染 json: @project.errors, 状态: :unprocessable_entity }结尾结尾def project_paramsparams.require(:project).permit(:id,:name,里程碑属性:[:id, {:milestone_ids =>[]}, {:ids =>[]}, {:names =>[]}, :project_id, :user_id,:name, :description, :due_date, :rank, :completed, :_destroy,tasks_attributes: [:id, {:task_ids =>[]}, {:names =>[]}, {:ids =>[]}, :milestone_id, :project_id,:user_id, :name, :description, :due_date, :rank, :completed, :_destroy]] )结尾结尾

形式测试 1:

<%= form_for @project, url: create_milestones_path(p: @project.id) do |f|%><label>里程碑</label><br><div class="row"><%= hidden_​​field_tag "project[names][]", nil %><% @milestones_templates.each 做 |m|%><%= check_box_tag "project[names][]", m.name, @milestones_templates.include?(m), id: dom_id(m)%><%= label_tag dom_id(m), m.name %><%= hidden_​​field_tag "project[里程碑][names][]", nil %><% m.task_templates.each 做 |t|%><%= check_box_tag "project[milestone][names][]", t.name, m.task_templates.include?(t), id: dom_id(t) %><%=label_tag dom_id(t), t.name %><%结束%><%结束%>

<%= f.submit %>

表单测试 2(尝试提交表单数组):

 
<%= hidden_​​field_tag "project[milestone_ids][]", nil %><% @milestones_templates.each 做 |m|%><div><%= f.fields_for :里程碑做 |fm|%><%= check_box_tag "project[milestone_ids][]", @milestones_templates.include?(m), id: dom_id(m) %><%= label_tag dom_id(m), m.name %></div><%= hidden_​​field_tag :name, m.name %><%= hidden_​​field_tag "project[里程碑][task_ids][]", nil %><% m.task_templates.each 做 |t|%><%= fm.fields_for :tasks do |ft|%><%= check_box_tag "project[里程碑][task_ids][]", t.name, m.task_templates.include?(t), id: dom_id(t)%><%=label_tag dom_id(t), t.name %><%结束%><%结束%><%结束%><%结束%>

根据 xcskier56 在评论中的请求,我添加了来自 Chrome 检查器的 POST 代码.如您所见,该表单甚至不调用任务,只调用父里程碑.里程碑显示在表单中,但任务没有....

project[formprogress]:2项目[里程碑_ids][]:项目[里程碑][名称]:真名称:里程碑1项目[里程碑][task_ids][]:项目[里程碑][名称]:真名称:里程碑2项目[里程碑][task_ids][]:项目[里程碑][名称]:真名称:里程碑 3项目[里程碑][task_ids][]:项目[里程碑][名称]:真名称:里程碑 4项目[里程碑][task_ids][]:

解决方案

这个代码我自己没测试过,不过我已经实现过类似的代码,所以思路应该是对的.

这里的技巧是使用each_with_index,然后将该索引传递给您的fields_for 调用.这样,您通过复选框添加的每个附加 milestone_id 将与之前的显着不同.您可以在此处找到另一个示例.

使用这种方法,您的表单应如下所示:

<%= form_for @project do |f|%><% @milestones_templates.each_with_index 做 |里程碑,索引|%><br><%= f.fields_for :milestones, index: index do |fm|%><%= fm.hidden_​​field :name, value: 里程碑.name %><!-- 创建一个复选框以将里程碑 ID 添加到项目中 --><%= fm.label 里程碑.name %><%= fm.check_box :milestone_template_id,{},里程碑.id %><br><%里程碑.task_templates.each_with_index do |task, another_index|%><%= fm.fields_for :tasks, index: another_index do |ft|%><!-- 为里程碑中的每个任务创建一个复选框--><%= ft.label task.name %><%= ft.check_box :task_ids, {}, task.id %><%结束%><%结束%><br><%结束%><%结束%><br><%= f.submit %><%结束%># 工作强参数.params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )

这应该输出milestone_template_ids,其中嵌套了每个task_template_ids.

我忘记了,如果您查看文档,check_boxes 中间需要另一个参数 f.checkbox :task_ids, task.id 实际上应该是以下内容:f.复选框:task_ids、{}、task.id

现在来看看答案.虽然这种形式确实有效,并且经过足够的摆弄,我认为您可以使用 rails 自动更新您的项目并通过嵌套属性,并创建您想要的所有内容,但我认为这不是一个好的设计.

使用构建器类是更好的设计.它只是一个 PORO(Plain Old Ruby Object).这将允许您做的是围绕构建器编写良好的测试.因此,您可以更加确信它始终有效,并且对 rails 的一些更改并没有破坏它.

这里有一些伪代码可以帮助您:

ProjectsController <<应用控制器定义更新@project = Project.find(params[:id])# 如果一切正常,这应该返回 true,并且结果 = ProjectMilestoneBuilder.perform(@project, update_params)如果结果 == 假# 构建器出了点问题结尾如果 result.errors.any?#处理成功别的# 处理失败# 项目没有更新,但事情没有爆炸.结尾结尾私人的定义更新参数params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )结尾结尾

在/lib/project_milestone_builder.rb 中

class ProjectMilestoneBuilderdef self.perform(项目,参数)里程碑参数 = 参数[:项目][:里程碑]里程碑参数.每个做 |m|#像这样的东西# 可能可以为此使用嵌套属性# Milestone.create(m)结尾返回 project.update_attributes(params)结尾结尾

在/spec/lib/project_milestone_builder_spec.rb 中

descibe ProjectMilestoneBuilder 做的# 创建模板和项目让(:模板){FactoryGirl.create:模板}让(:项目){FactoryGirl.create:项目,模板:模板}# 创建用于更新项目的参数.# 这将必须有动态代码段才能在那里获得适当的里程碑模板 ID让(:参数){{项目:{里程碑......"})描述 '#perform' 做让(:结果){ ProjectMilestoneBuilder.perform(项目,参数)}它 {expect(result.id).to eq project.id}# ...结尾结尾

使用此模式,您最终将得到一个封装良好、易于测试的类,该类将完全按照您的预期执行.快乐编码.

3/13 UPDATE:
I've made a small sample project with my models, controller logic and several form versions.



I am building a form where a user can add "Tasks" and "Milestones" together. (ie. Task = 'Vacuum' is inside Milestone = 'clean House'). It's basically a Task/Subtask type model with the parent being 'Milestone' and the child being 'Task'.

Both Tasks and Milestones belong to "Project"....so I am trying to add the Tasks and Milestones through a nested form with an update action. I am thinking the way to go is create a form for each @task_template instance and update multiple forms at once.

My problem is that I am also trying to dynamically set "starter milestones/tasks" through tables called "MilestoneTemplates" and "TaskTemplates"....

The user pulls up the "Add Milestones/Task" page and, depending on their type of project, they see an array of prebuilt tasks(@task_templates) & milestones(@milestone_templates) next to checkboxes. The user then CHECKS the checkbox next to the task or milestone they would like to add. This should create a specific task for the User with a prebuilt @task_template.name, @task_template.description...etc

I cannot get this to even create 1. I am using Rails 4 and I think I have set my strong_params correctly. Below is where I am on this:

Models:

class Task < ActiveRecord::Base
    belongs_to :user
    belongs_to :project
  belongs_to :milestone

class Milestone < ActiveRecord::Base
 belongs_to :project
 belongs_to :user
 has_many :tasks, dependent: :destroy, inverse_of: :milestone
 accepts_nested_attributes_for :tasks, allow_destroy: true

class Project < ActiveRecord::Base
 has_many :milestones, dependent: :destroy
 has_many :tasks, dependent: :destroy
 accepts_nested_attributes_for :tasks, allow_destroy: true
 accepts_nested_attributes_for :milestones, allow_destroy: true

 #the "Starter Milestones & Tasks"

class MilestoneTemplate < ActiveRecord::Base
    has_many :task_templates, dependent: :destroy, inverse_of: :milestone_template

class TaskTemplate < ActiveRecord::Base
     belongs_to :milestone_template,  inverse_of: :task_templates

Controller:

class ProjectsController < ApplicationController

def new_milestones
 @project = Project.find(params[:p])
 @project.milestones.build
 @project.tasks.build
 @milestones_templates = MilestoneTemplate.where(template_id: @project.template_id)
end

def create_milestones
 @project.milestone_ids = params[:project][:milestones]
 @project.task_ids = params[:project][:tasks]
 @milestone = Milestone.new
 @task = Task.new
 @template = Template.find( @project.template_id)
  if @project.update_attributes(project_params)
    redirect_to  view_milestones_path(p: @project.id)
    flash[:notice] = "Successfully Added Tasks & Milestones"
  else
    redirect_to  new_milestones_path(p:  @project.id )
    format.json { render json: @project.errors, status: :unprocessable_entity }
  end
end

def project_params
      params.require(:project).permit( :id, :name,
        milestones_attributes: [:id, {:milestone_ids => []}, {:ids => []}, {:names => []}, :project_id, :user_id,
            :name, :description, :due_date, :rank, :completed, :_destroy,
        tasks_attributes: [:id, {:task_ids => []}, {:names => []},  {:ids => []}, :milestone_id, :project_id,    
          :user_id, :name, :description, :due_date, :rank, :completed,  :_destroy]] )
end
end

Form Test 1:

<%= form_for @project, url: create_milestones_path(p: @project.id) do |f| %>
     <label>Milestones</label><br>
     <div class="row">
       <%= hidden_field_tag "project[names][]", nil %>
       <% @milestones_templates.each do |m| %>
         <%= check_box_tag  "project[names][]", m.name, @milestones_templates.include?(m), id: dom_id(m)%> 
         <%= label_tag dom_id(m), m.name  %>

           <%= hidden_field_tag "project[milestone][names][]", nil %>
           <% m.task_templates.each do |t| %>
             <%= check_box_tag  "project[milestone][names][]", t.name, m.task_templates.include?(t), id: dom_id(t) %> 
             <%= label_tag dom_id(t), t.name  %>
           <% end %>
       <% end %>
     </div>
 <%= f.submit %>

Form Test 2(trying to submit an array of forms):

 <label>Milestones</label><br>
   <%= hidden_field_tag "project[milestone_ids][]", nil %>
   <% @milestones_templates.each do |m| %>
   <div>
      <%= f.fields_for :milestones do |fm|%>
         <%= check_box_tag    "project[milestone_ids][]",  @milestones_templates.include?(m), id: dom_id(m) %> 
         <%= label_tag dom_id(m), m.name  %></div>
      <%= hidden_field_tag :name, m.name %>
      <%= hidden_field_tag "project[milestone][task_ids][]", nil %>

         <% m.task_templates.each do |t| %>
         <%= fm.fields_for :tasks do |ft| %>
               <%= check_box_tag  "project[milestone][task_ids][]", t.name,  m.task_templates.include?(t), id: dom_id(t)%> 
               <%= label_tag dom_id(t), t.name  %>
         <% end %>
         <% end %>
      <% end %>
   <% end %>
   </div>

as per xcskier56's request in the comments, I've added my POST code from Chrome inspector. As you can see, the form isn't even calling the Tasks, just the parent Milestones. The Milestones show up in the form, but the tasks don't....

project[formprogress]:2
project[milestone_ids][]:
project[milestone][names]:true
name:Milestone 1
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 2
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 3
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 4
project[milestone][task_ids][]:

解决方案

I haven't been able to test this code myself, but I have implemented similar code, so the ideas should be correct.

The trick here is using each_with_index, and then passing that index to your fields_for call. This way the each additional milestone_id that you add via a checkbox will be significantly different from the previous. You can find another example of this here.

Using this approach, your form should look something like this:

<%= form_for @project do |f| %>
  <% @milestones_templates.each_with_index do |milestone, index| %>
    <br>
    <%= f.fields_for :milestones, index: index do |fm| %>
      <%= fm.hidden_field :name, value: milestone.name %>
      <!-- Create a checkbox to add the milestone_id to the project -->
      <%= fm.label milestone.name %>
      <%= fm.check_box :milestone_template_id,{}, milestone.id %>
      <br>
      <% milestone.task_templates.each_with_index do |task, another_index| %>
        <%= fm.fields_for :tasks, index: another_index do |ft| %>
          <!-- Create a checkbox for each task in the milestone -->
          <%= ft.label task.name %>
          <%= ft.check_box :task_ids, {}, task.id %>
        <% end %>
      <% end %>
      <br>
    <% end %>
  <% end %>
  <br>
<%= f.submit %>
<% end %>

# Working strong parameters.
params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )

This should output the milestone_template_ids with each of those's task_template_ids nested inside.

Edit: I forgot that if you look at the docs, the check_boxes need another param in the middle f.checkbox :task_ids, task.id should actually be the following: f.checkbox :task_ids, {}, task.id

Now for the meat of the answer. While this form does work, and given enough fiddling I think you could get rails to automagically update your project and through nested attributes, and create everything that you want it to, I don't think this is a good design.

What is a far better design is using a builder class. It is just a PORO (Plain Old Ruby Object). What this will allow you to do is write good tests around the builder. So you can be much more assured that it will always work, and that some change to rails didn't break it.

Here's some pseudo code to get you going:

ProjectsController << ApplicationController

  def update
    @project = Project.find(params[:id])
    # This should return true if everything works, and 
    result = ProjectMilestoneBuilder.perform(@project, update_params)
    if result == false
      # Something went very wrong in the builder
    end
    if result.errors.any?
      #handle success
    else
      # handle failure
      # The project wasn't updated, but things didn't explode.
    end
  end

  private

  def update_params
    params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )
  end
end

In /lib/project_milestone_builder.rb

class ProjectMilestoneBuilder 
  def self.perform(project, params)
    milestone_params = params[:project][:milestones]
    milestone_params.each do |m|
      # Something like this
      # Might be able to use nested attributes for this
      # Milestone.create(m)
    end

    return project.update_attributes(params)
  end
end

In /spec/lib/project_milestone_builder_spec.rb

descibe ProjectMilestoneBuilder do
  # Create a template and project
  let(:template) {FactoryGirl.create :template}
  let(:project) {FactoryGirl.create :project, template: template}

  # Create the params to update the project with. 
  # This will have to have dynamic code segments to get the appropriate milestone_template_ids in there
  let(:params) { "{project: {milestones ..." })

  descibe '#perform' do
    let(:result) { ProjectMilestoneBuilder.perform(project, params) }
    it {expect(result.id).to eq project.id}
    # ...
  end
end

With this pattern, you will end up with a very well encapsulated, easily testable class that will do exactly what you expect it to do. Happy coding.

这篇关于通过复选框添加多个嵌套属性 Rails 4(可能有多种形式)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆