Rails MVC 架构 - 具有多个控制器源的视图
Rails MVC Architecture - View with multiple controller sources
我的 rails 应用程序中有一个页面是一个控制器的显示操作,但它包含一个提交为另一个控制器创建的表单。这对于简单地向第二个控制器提交创建表单来说很好,但是因为表单包含在备用控制器的显示操作中,所以我不能轻易地在表单上显示验证错误。这是结构的基本概念:
class VendorsController < ApplicationController
def show
@vendor = […]
end
end
<!-- vendors_controller/show -->
<h1><%= @vendor.name %></h1>
<%= form_with model: Order, local: true do |f| %>
<%= f.hidden_field :vendor_id, value: @vendor.id %>
[…]
<% end %>
class OrdersController < ApplicationController
def create
@order = […]
if @order.save
[…]
else
render 'new' # Not where user came from, actually want to go to vendors_controller/show
end
end
end
我发现在 vendors_controller/show
的“新”上下文中处理创建订单的唯一方法是将 OrdersController#create
移动到 [=17] 中的自定义 create_order
方法=],但是这会将模型的核心 REST 逻辑移动到不同的控制器,这感觉就像是一个主要的代码味道。来自移动工程背景,我们可以将我们的屏幕分成多个子视图,并为不同的元素使用独立的控制器,但是当然这在网络上并不可行,因为单个控制器处理传入的请求和它的响应。
构建此行为的正确方法是什么,以便我可以保留 OrdersController
的大部分 REST 功能,同时还将新操作“嵌入”到另一个控制器拥有的视图中?我想将 Order
相关逻辑保留在 OrdersController
中,但重定向回 VendorsController#show
会破坏验证错误,将来我可能还想要一个具有相同逻辑的 OrdersController#new
路由但与 VendorsController
.
分开
我会调查 accepts_nested_attributes_for
There's a railscasts episode on this 解释了如何在保留 RESTfulness 的同时进行嵌套表单
你或多或少会做这样的事情:
<!-- vendors_controller/show -->
<h1><%= @vendor.name %></h1>
<%= fields_for :orders do |builder| %>
<%= render 'order_fields', f: builder %>
<% end %>
然后将订单表单字段放入新的部分:
<!-- app/views/vendors/_order_fields.html.erb -->
<%= f.hidden_field :vendor_id, value: @vendor.id %>
<!-- and the rest of the order fields, too... -->
并且不要忘记在 vendor#show controller logic
中包含像 @vendor.orders.build
这样的行
您没有针对资源的单独 new
操作实际上是一种相当常见的情况。而且您在 Rails 方面的推理是错误的,因为它实际上只是关于您正在呈现和发回的视图以响应请求。
您可以通过呈现新视图(可能不是您正在寻找的用户体验)或使用 AJAX 提交表单并替换元素来处理此处的无效表单提交:
// app/views/orders/new.js.erb
document.querySelector("#order_form")
.innerHTML("<%= j(render(partial: 'form', locals: { order: @order })) %>");
# app/views/orders/_form.html.erb
# removed "local: true" so thats its sent as a XHR request
<%= form_with model: order, html: { id: "order_form" } do |f| %>
<%= f.hidden_field :vendor_id %> # smelly AF - use a nested route instead
<% end %>
class OrdersController < ApplicationController
def create
@order = […]
if @order.save
[…]
else
respond_to do |f|
f.js { render :new }
end
end
end
end
如果您不想走 Server Side Concerns(又名 js.erb spagetti)路线,您可以执行 JSON 响应并处理在客户端呈现错误。
虽然可以从 OrdersController#create
方法而不是渲染 vendors/show.html.erb
视图,但如果验证失败,这是一个彻头彻尾的不合时宜的提议,因为它使控制器负责渲染完全不同的资源。
虽然在这个简单的示例中这并不是真正的问题,但如果您向“vendors/show”视图添加功能,您将不得不重复设置要传递给视图的数据的逻辑在两个地方。
即使您确实通过使用水平继承(顾虑)减少了潜在的代码重复问题,您仍然在践踏单一职责原则。
class OrdersController < ApplicationController
# POST /vendors/:vendor_id/orders
def create
@vendor = Vendor.find(params[:vendor_id])
@order = […]
if @order.save
[…]
else
render "vendors/show"
end
end
end
让 VendorsController 处理创建订单的情况更糟,甚至不应该考虑。
我的 rails 应用程序中有一个页面是一个控制器的显示操作,但它包含一个提交为另一个控制器创建的表单。这对于简单地向第二个控制器提交创建表单来说很好,但是因为表单包含在备用控制器的显示操作中,所以我不能轻易地在表单上显示验证错误。这是结构的基本概念:
class VendorsController < ApplicationController
def show
@vendor = […]
end
end
<!-- vendors_controller/show -->
<h1><%= @vendor.name %></h1>
<%= form_with model: Order, local: true do |f| %>
<%= f.hidden_field :vendor_id, value: @vendor.id %>
[…]
<% end %>
class OrdersController < ApplicationController
def create
@order = […]
if @order.save
[…]
else
render 'new' # Not where user came from, actually want to go to vendors_controller/show
end
end
end
我发现在 vendors_controller/show
的“新”上下文中处理创建订单的唯一方法是将 OrdersController#create
移动到 [=17] 中的自定义 create_order
方法=],但是这会将模型的核心 REST 逻辑移动到不同的控制器,这感觉就像是一个主要的代码味道。来自移动工程背景,我们可以将我们的屏幕分成多个子视图,并为不同的元素使用独立的控制器,但是当然这在网络上并不可行,因为单个控制器处理传入的请求和它的响应。
构建此行为的正确方法是什么,以便我可以保留 OrdersController
的大部分 REST 功能,同时还将新操作“嵌入”到另一个控制器拥有的视图中?我想将 Order
相关逻辑保留在 OrdersController
中,但重定向回 VendorsController#show
会破坏验证错误,将来我可能还想要一个具有相同逻辑的 OrdersController#new
路由但与 VendorsController
.
我会调查 accepts_nested_attributes_for
There's a railscasts episode on this 解释了如何在保留 RESTfulness 的同时进行嵌套表单
你或多或少会做这样的事情:
<!-- vendors_controller/show -->
<h1><%= @vendor.name %></h1>
<%= fields_for :orders do |builder| %>
<%= render 'order_fields', f: builder %>
<% end %>
然后将订单表单字段放入新的部分:
<!-- app/views/vendors/_order_fields.html.erb -->
<%= f.hidden_field :vendor_id, value: @vendor.id %>
<!-- and the rest of the order fields, too... -->
并且不要忘记在 vendor#show controller logic
中包含像@vendor.orders.build
这样的行
您没有针对资源的单独 new
操作实际上是一种相当常见的情况。而且您在 Rails 方面的推理是错误的,因为它实际上只是关于您正在呈现和发回的视图以响应请求。
您可以通过呈现新视图(可能不是您正在寻找的用户体验)或使用 AJAX 提交表单并替换元素来处理此处的无效表单提交:
// app/views/orders/new.js.erb
document.querySelector("#order_form")
.innerHTML("<%= j(render(partial: 'form', locals: { order: @order })) %>");
# app/views/orders/_form.html.erb
# removed "local: true" so thats its sent as a XHR request
<%= form_with model: order, html: { id: "order_form" } do |f| %>
<%= f.hidden_field :vendor_id %> # smelly AF - use a nested route instead
<% end %>
class OrdersController < ApplicationController
def create
@order = […]
if @order.save
[…]
else
respond_to do |f|
f.js { render :new }
end
end
end
end
如果您不想走 Server Side Concerns(又名 js.erb spagetti)路线,您可以执行 JSON 响应并处理在客户端呈现错误。
虽然可以从 OrdersController#create
方法而不是渲染 vendors/show.html.erb
视图,但如果验证失败,这是一个彻头彻尾的不合时宜的提议,因为它使控制器负责渲染完全不同的资源。
虽然在这个简单的示例中这并不是真正的问题,但如果您向“vendors/show”视图添加功能,您将不得不重复设置要传递给视图的数据的逻辑在两个地方。
即使您确实通过使用水平继承(顾虑)减少了潜在的代码重复问题,您仍然在践踏单一职责原则。
class OrdersController < ApplicationController
# POST /vendors/:vendor_id/orders
def create
@vendor = Vendor.find(params[:vendor_id])
@order = […]
if @order.save
[…]
else
render "vendors/show"
end
end
end
让 VendorsController 处理创建订单的情况更糟,甚至不应该考虑。