65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2013年2月11日

CPOL

7分钟阅读

viewsIcon

19397

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

概述

在我们上次结束时,他们一路披荆斩棘,穿越了我们应用程序的上层结构,并准备将其彻底摧毁。击败了TIE战斗机屏幕后,我们勇敢的飞行员们已经下降到了星际战斗机识别指南的战壕中。他们的计算机已锁定,他们正在接收信号……

在本文中,我们将利用我们在此此处实现的服务器端代码和连接。我们将审视我们设置的视图以及一些JavaScript,让我们的星际战斗机识别指南变得更加花哨。

稳定你的后部偏转器,留意敌方战斗机——这里将有价值一个图拉塞炮塔的服务器端代码。

索引视图

星际战斗机识别指南的主页/登陆页(即我们的index.html.erb)没有太多有趣的东西。它基本上是一个显示到目前为止已录入的所有Ships的表格。这是主页外观的精美图片

Home page of the Starfighter Recognition Guide

我知道——很酷,对吧?  如果您想查看index.html.erb视图的源代码,可以在此处查看。

_form 部分视图

因为添加新Ship或编辑现有Ship的功能在很大程度上是相同的,所以我创建了一个_form.html.erb部分视图,它封装了我们持久化ShipsPilots所需的所有数据输入字段和其他附加功能。它看起来像这样

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(即Shipform)上使用fields_for方法来生成所有ShipPilots的输入字段。我们来看看_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的截图

Editing a Ship with Pilots

添加新的ShipPilot

如前所述,添加新Ship与编辑现有Ship几乎相同。让我们在星际战斗机识别指南中添加一艘新飞船:TIE拦截机。我们在主页/登陆页上点击“添加飞船”按钮,然后得到类似这样的结果

Add the Tie Interceptor

现在我们点击“添加飞行员”按钮,它将我们的_pilot_fields.html.erb部分视图渲染为一个模态表单。我们在上一篇文章中讨论了实现这一功能的机制(我们的link_to_add_fields辅助方法)。下面是允许我们添加新Pilot的模态表单的截图

 Add a Pilot to the Tie Interceptor with a modal form

下面是_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对象字面量,它有一个函数:initinit函数连接了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对象由formHandlerrowBuilder引用。这为我提供了一个绝佳的过渡到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比他的同类cfgformHandler要复杂一些。有些rowBuilder的属性/方法不需要被任何人调用,所以我想将它们保持私有。为了实现这一点,我使用了揭示模块模式(听起来很棒,对吧?!),只暴露应该被…呃…暴露的方法,我想。我非常喜欢这种模式,并且我认为它物有所值。它易于实现,使代码更易读,并且清晰地向其他开发人员传达了意图。通过额外的一两行代码,我们就告诉了下一个在该项目上工作的家伙/女孩,rowBuilder的哪些部分是内部功能/管道,哪些部分是供我们程序其他组件使用的。

代码异味rowBuilder.link属性可能应该是私有的,并且在返回新<TR>元素之前就应该被添加到它。目前,rowBuilder.link属性在formHandler.appendFields()方法(formHandler片段的第12行)中使用。这是重构的绝佳机会。

有了所有这些JavaScript,当用户在向TIE拦截机Ship添加新Pilot后点击“添加”按钮时会发生什么?屏幕看起来是这样的

Add a Ship with a Pilot

总结:已出发!

那么……就是这样了。我们创建了一个新的TIE拦截机Ship,其中有一个名为Maarek Stele的Pilot。由于我们的实现,用户可以在将所有内容提交到数据库之前更改任何Pilot属性。当用户点击“保存”按钮时,POST将包含ShipPilot属性(同样,在此处可以找到POST示例)。

反方辩手:我的范围是负面的,我什么也看不到!

嘿 Jeff,为什么一开始你不直接将一个带有新输入字段的行附加到“飞行员”表中呢?搞这么多模态的鬼把戏干嘛?
当然,我们可以这样做。如果您还记得很久以前在一篇远得不能再远的帖子中,我们的任务是使用模态数据输入表单将“子”对象添加到“父”对象。此外,我认为模态数据输入表单提供了更好的用户体验。添加一个新数据输入字段行会有点不显眼,您不觉得吗?如果您在用户面前弹出一个模态表单,就不会对正在发生的事情感到困惑。非常清楚(我希望如此)用户应该为Pilot提供数据并将其添加到列表中。

参考

  • Railscast 197accepts_nested_attributes_forfields_for方面对我的帮助很大。
  • 星际战斗机识别指南的源代码可以在此处找到。 
© . All rights reserved.