Rails 3.2:嵌套表单演示,第三部分:我们开始攻击!





4.00/5 (1投票)
Rails 3.2:嵌套表单演示,第三部分:我们开始攻击!
概述
在我们上次结束时,他们一路披荆斩棘,穿越了我们应用程序的上层结构,并准备将其彻底摧毁。击败了TIE战斗机屏幕后,我们勇敢的飞行员们已经下降到了星际战斗机识别指南的战壕中。他们的计算机已锁定,他们正在接收信号……
在本文中,我们将利用我们在此处和此处实现的服务器端代码和连接。我们将审视我们设置的视图以及一些JavaScript,让我们的星际战斗机识别指南变得更加花哨。
稳定你的后部偏转器,留意敌方战斗机——这里将有价值一个图拉塞炮塔的服务器端代码。
索引视图
星际战斗机识别指南的主页/登陆页(即我们的index.html.erb
)没有太多有趣的东西。它基本上是一个显示到目前为止已录入的所有Ships
的表格。这是主页外观的精美图片
我知道——很酷,对吧? 如果您想查看index.html.erb视图的源代码,可以在此处查看。
_form 部分视图
因为添加新Ship
或编辑现有Ship
的功能在很大程度上是相同的,所以我创建了一个_form.html.erb
部分视图,它封装了我们持久化Ships
和Pilots
所需的所有数据输入字段和其他附加功能。它看起来像这样
app/views/ships/_form.html.erb
<%= form_for(@ship, :html => {:class => "form form-horizontal"}) do |f| %>
<fieldset>
<legend><%= "#{ @ship.name } " unless @ship.name.nil? %>Ship Information</legend>
<%= render('error_messages', :object => f.object) %>
<div class="control-group">
<%= f.label(:name, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:name, :class => "input-xlarge") %>
</div>
</div>
<div class="control-group">
<%= f.label(:crew, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:crew, :class => "input-xlarge") %>
</div>
</div>
<div class="control-group">
<%= f.label(:has_astromech, :class => "control-label") %>
<div class="controls">
<%= f.check_box(:has_astromech, :class => "checkbox") %>
</div>
</div>
<div class="control-group">
<%= f.label(:speed, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:speed, :class => "input-xlarge") %>
</div>
</div>
<div class="control-group">
<%= f.label(:armament, :class => "control-label") %>
<div class="controls">
<%= f.text_area(:armament, :class => "input-xlarge", :rows => "3") %>
</div>
</div>
</fieldset>
<fieldset>
<legend>Pilots <%= "That Fly the #{ @ship.name }" unless @ship.name.nil? %></legend>
<%= render('pilots_table', :f => f) %>
<%= link_to_add_fields("Add a Pilot", f, :pilots, :class => "btn btn-primary", :title => "Add a new Pilot to the list of Pilots that fly this Ship.") %>
<p></p>
</fieldset>
<div class="modal-footer">
<%= f.submit("Save", :class => "btn btn-primary", :title => "Save the changes to this Ship.") %>
<%= link_to("Cancel", ships_path, :confirm => "Are you sure you want to cancel? Any changes will be lost.", :class => "btn btn-inverse", :title => "Cancel the changes and return to the Home page.") %>
</div>
<% end %>
有趣的部分在第43行,我们在这里渲染了_pilots_table.html.erb
部分视图。请注意我们是如何将form
传递给它的?这将允许我们在“父”form
(即Ship
的form
)上使用fields_for
方法来生成所有Ship
的Pilots
的输入字段。我们来看看_pilots_table.html.erb
部分视图,好吗?是的,让我们看看
app/views/ships/_pilots_table.html.erb
<table id="pilots-table" class="table table-hover table-striped">
<thead>
<th>First Name</th>
<th>Last Name</th>
<th>Call Sign</th>
<th></th>
</thead>
<tbody>
<%= f.fields_for(:pilots) do |pilots_form| %>
<tr class="fields">
<td><%= pilots_form.text_field(:first_name) %></td>
<td><%= pilots_form.text_field(:last_name) %></td>
<td><%= pilots_form.text_field(:call_sign) %></td>
<% if current_page?(new_ship_path) || current_page?(edit_ship_path) %>
<td>
<%= link_to_remove_fields('<i class="icon-remove"></i>'.html_safe, pilots_form, :title => "Delete this Pilot.") %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
在第9行,我们调用fields_for
来为与我们的Ship
关联的每个Pilot
生成输入字段。另外请注意,在第16行,我们使用了我们的link_to_remove_fields
辅助方法(在此详尽描述)来创建一个链接,允许我们根据需要删除Pilot
。因为我们使用fields_for
连接了关联Pilots
的输入字段,所以我们可以编辑任何Pilots
和/或Ship
的属性。当我们提交form
时,一切都将被一次性保存到数据库中。要查看此form
提交时POST可能是什么样子,您可以在此处查看。
为了让您看到实际效果,让我们来看一张正在编辑Ship
的截图
添加新的Ship
和Pilot
如前所述,添加新Ship
与编辑现有Ship
几乎相同。让我们在星际战斗机识别指南中添加一艘新飞船:TIE拦截机。我们在主页/登陆页上点击“添加飞船”按钮,然后得到类似这样的结果
现在我们点击“添加飞行员”按钮,它将我们的_pilot_fields.html.erb
部分视图渲染为一个模态表单。我们在上一篇文章中讨论了实现这一功能的机制(我们的link_to_add_fields
辅助方法)。下面是允许我们添加新Pilot
的模态表单的截图
下面是_pilot_fields.html.erb在幕后是什么样子
app/views/ships/_pilot_fields.html.erb
<div id="new-pilot-fields" class="modal fade" data-backdrop="static">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3>Add a Pilot<%= " to the #{ @ship.name } Pilot Roster" unless @ship.name.nil? %></h3>
</div>
<div class="modal-body">
<fieldset>
<div class="control-group">
<%= f.label(:first_name, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:first_name, :class => "input-xlarge field") %>
</div>
</div>
<div class="control-group">
<%= f.label(:last_name, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:last_name, :class => "input-xlarge field") %>
</div>
</div>
<div class="control-group">
<%= f.label(:call_sign, :class => "control-label") %>
<div class="controls">
<%= f.text_field(:call_sign, :class => "input-xlarge field") %>
</div>
</div>
<%= f.hidden_field(:_destroy, :class => "field") %>
</fieldset>
</div>
<div class="modal-footer">
<button id="addButton" type="button" class="btn btn-primary" data-dismiss="modal" aria-hidden="true" title="Add this Pilot to the list of Pilots that are assigned to this Ship.">Add</button>
<button id="cancelButton" type="button" class="btn btn-inverse" data-dismiss="modal" aria-hidden="true" title="Close this screen without adding the Pilot to the list.">Cancel</button>
</div>
</div>
<script type="text/javascript">
pilotFieldsUI.init();
</script>
_pilot_fields.html.erb部分视图没有什么特别花哨的地方。我甚至可以说唯一有趣的部分是底部的script
标签。那里发生了什么?让我们找出答案!
Pilot
模态表单的JavaScript
所以,我们有了出现在模态表单中的Pilot
输入字段。现在呢?当用户点击模态表单上的“添加”按钮时会发生什么?用户如何能够向单个Ship
添加多个Pilots
?
好吧,如果新创建的Pilot
被添加到“添加飞船…”屏幕(如上所示)的“飞行员”表中,那就太好了。理想情况下,它看起来应该与“编辑…”屏幕(也如上所示)中的“飞行员”表完全相同。
app/assets/javascripts/ships.js
var pilotFieldsUI = { init: function() { $('#addButton').on('click', function() { formHandler.appendFields(); formHandler.hideForm(); }); } };
上面的代码显示了我们的pilotFieldsUI
对象字面量,它有一个函数:init
。init
函数连接了Pilot
模态表单上的addButton
。正如您所见,当用户点击Pilot
模态表单上的“添加”按钮时,将调用几个方法。我们接下来看看
app/assets/javascripts/ships.js
var formHandler = { // Public method for adding a new row to the table. appendFields: function() { // Get a handle on all the input fields in the form and detach them from the DOM (we will attach them later). var inputFields = $(cfg.formId + ' ' + cfg.inputFieldClassSelector); inputFields.detach(); // Build the row and add it to the end of the table. rowBuilder.addRow(cfg.getTBodySelector(), inputFields); // Add the "Remove" link to the last cell. rowBuilder.link.appendTo($('tr:last td:last')); }, // Public method for hiding the data entry fields. hideForm: function() { $(cfg.formId).modal('hide'); } };
同样,formHandler
没什么特别之处。formHandler
的想法是这样的:formHandler
负责管理所有花哨的UI事务。它会将具体操作(例如,使用Pilot
输入字段构建一个行元素)委托给其他对象。在这种情况下,我们想构建一个我们将添加到“飞行员”表中的行,并且完成后我们要隐藏模态表单。为了实现这一点,我们有几个方法
- appendFields:此方法将从
Pilot
模态表单的输入字段添加到Ship
表单的“飞行员”表中,作为新的一行。 - hideForm:隐藏/关闭
Pilot
模态表单(感谢Twitter Bootstrap!)。
我想将我所有的ID和其他“魔术字符串”类型的东西放在一个地方,所以我创建了一个快速的cfg
对象字面量,看起来像下面这样
app/assets/javascripts/ships.js
var cfg = { formId: '#new-pilot-fields', tableId: '#pilots-table', inputFieldClassSelector: '.field', getTBodySelector: function() { return this.tableId + ' tbody'; } };
cfg
对象由formHandler
和rowBuilder
引用。这为我提供了一个绝佳的过渡到rowBuilder
,它看起来是这样的
app/assets/javascripts/shipsjs
var rowBuilder = function() { // Private property that define the default <TR> element text. var row = $('<tr>', { class: 'fields' }); // Public property that describes the "Remove" link. var link = $('<a>', { href: '#', onclick: 'remove_fields(this); return false;', title: 'Delete this Pilot.' }).append($('<i>', { class: 'icon-remove' })); // A private method for building a <TR> w/the required data. var buildRow = function(fields) { var newRow = row.clone(); $(fields).map(function() { $(this).removeAttr('class'); return $('<td/>').append($(this)); }).appendTo(newRow); return newRow; } // A public method for building a row and attaching it to the end of a <TBODY> element. var attachRow = function(tableBody, fields) { var row = buildRow(fields); $(row).appendTo($(tableBody)); } // Only expose public methods/properties. return { addRow: attachRow, link: link } }();
rowBuilder
只有一个目的——构建一个将被添加到“飞行员”表中的<TR>
元素。该<TR>
元素将包含来自Pilot
模态表单的输入字段。
rowBuilder
比他的同类cfg
或formHandler
要复杂一些。有些rowBuilder
的属性/方法不需要被任何人调用,所以我想将它们保持私有。为了实现这一点,我使用了揭示模块模式(听起来很棒,对吧?!),只暴露应该被…呃…暴露的方法,我想。我非常喜欢这种模式,并且我认为它物有所值。它易于实现,使代码更易读,并且清晰地向其他开发人员传达了意图。通过额外的一两行代码,我们就告诉了下一个在该项目上工作的家伙/女孩,rowBuilder
的哪些部分是内部功能/管道,哪些部分是供我们程序其他组件使用的。
代码异味:rowBuilder.link
属性可能应该是私有的,并且在返回新<TR>
元素之前就应该被添加到它。目前,rowBuilder.link
属性在formHandler.appendFields()
方法(formHandler
片段的第12行)中使用。这是重构的绝佳机会。
有了所有这些JavaScript,当用户在向TIE拦截机Ship
添加新Pilot
后点击“添加”按钮时会发生什么?屏幕看起来是这样的
总结:已出发!
那么……就是这样了。我们创建了一个新的TIE拦截机Ship
,其中有一个名为Maarek Stele的Pilot
。由于我们的实现,用户可以在将所有内容提交到数据库之前更改任何Pilot
属性。当用户点击“保存”按钮时,POST将包含Ship
和Pilot
属性(同样,在此处可以找到POST示例)。
反方辩手:我的范围是负面的,我什么也看不到!
嘿 Jeff,为什么一开始你不直接将一个带有新输入字段的行附加到“飞行员”表中呢?搞这么多模态的鬼把戏干嘛?
当然,我们可以这样做。如果您还记得很久以前在一篇远得不能再远的帖子中,我们的任务是使用模态数据输入表单将“子”对象添加到“父”对象。此外,我认为模态数据输入表单提供了更好的用户体验。添加一个新数据输入字段行会有点不显眼,您不觉得吗?如果您在用户面前弹出一个模态表单,就不会对正在发生的事情感到困惑。非常清楚(我希望如此)用户应该为Pilot
提供数据并将其添加到列表中。
参考
- Railscast 197在
accepts_nested_attributes_for
和fields_for
方面对我的帮助很大。 - 星际战斗机识别指南的源代码可以在此处找到。