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

Rails 3.2:嵌套表单演示 第4部分:切换到目标计算机!

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2013年2月26日

CPOL

7分钟阅读

viewsIcon

13021

我们将介绍如何为我们的 Ship 和 Pilot 模型添加一些验证规则。

概述

我们的英雄们一路浴血奋战,穿过了死星的战斗机群,看到了那条战壕。当他们进入战壕时,飞行员们稳定了他们的偏转护盾,准备迎接即将到来的、毫无疑问会射向他们的涡轮激光的洪流。

在这篇文章中,我们将介绍如何为我们的 ShipPilot 模型添加一些验证规则。希望这些努力将阻止我们的用户……嗯……随心所欲地使用我们温顺而天真的星际战斗机识别指南。我们的排气口很小,如果被一些笨手笨脚的用户损坏了,那就太可惜了。

验证

在我们 上一篇文章 中,我们完成了星际战斗机识别指南,它可以保存 Ship 及其关联的 Pilots 的所有更改(创建/更新/删除)。很酷,对吧?

在我们随意保存一切之前,我们最好检查一下用户是否提供了我们需要的数据。让我们为我们的应用程序添加一些数据输入要求,好吗?我们需要做的是:

Ship 要求

  • 一艘 Ship 必须有名称。
  • 一艘 Ship 的名称必须在 3 到 50 个字符之间。
  • 一艘 Ship 必须有船员。
  • 一艘 Ship 必须有 1 到 5 名船员。
    • 任意的?当然。但我认为,如果一艘 Ship 有超过 5 名船员,它可能是一艘主力舰,或者其规模不适合星际战斗机识别指南。
  • 一艘 Ship 必须有速度。
  • 一艘 Ship 的速度必须在 50 到 200 之间……嗯……东西每……呃……时间单位。
    • 冒着被取消星战宅卡片的风险,我不确定星际战斗机的速度是如何测量的。是的,我查了。不,我没有找到任何确凿的证据。

下面是完整的 Ship 模型
app/models/ship.rb

class Ship < ActiveRecord::Base   attr_accessible :armament, :crew, :has_astromech, :name, :speed   attr_accessible :pilots_attributes   has_many :pilots   accepts_nested_attributes_for :pilots,                                 :reject_if => :all_blank,
                                :allow_destroy => true

  validates :name,
            :presence => true,
            :uniqueness => { :case_sensitive => false },
            :length => { :maximum => 50, :minimum => 3 }

  validates :crew,
            :presence => true,
            :inclusion => { :in => 1..5, :message => "must be between 1 and 5" }

  validates :speed,
            :presence => true,
            :inclusion => { :in => 50..200, :message => "must be between 50 and 200" }
end

重构: 上面的代码与 第一篇文章 相比有所改变。特别是,我不得不将 :reject_iflambda { |attrs| attrs.all? { |key, value| value.blank? } 更改为 :all_blank。在阅读了关于 accepts_nested_attributes_for 的文档后,我使用的 Proc 并不够好。你为什么问?嗯,我们的 Pilot 对象有它们特殊的 _destroy 字段。_destroy 属性只能是 truefalse,所以它永远不会是空白的。上面的 Proc 会允许我们保存“空的”Pilot 记录(即 first_namelast_namecall_sign 值都为 nullPilots)。:all_blank 符号指向一个 ActiveRecord Proc,它解决了这个问题。使用 :reject_if => :all_blank 意味着任何没有数据的 Pilot 记录在保存时都会被忽略,这就是我们想要的。所以……太好了。

Pilot 要求

  • 一架 Pilot 必须有名字。
  • 一架 Pilot 的名字不能超过 50 个字符。
  • 一架 Pilot 必须有姓氏。
  • 一架 Pilot 的姓氏不能超过 50 个字符。
  • 一架 Pilot 必须有一个呼号。
  • 一架 Pilot 的呼号必须是唯一的。这里不区分大小写——“red 5”与“Red 5”相同,与“RED 5”相同,等等。
    • 为了确保这个要求得到执行,已经在 pilots 表的 call_sign 列上添加了一个唯一索引。你可以在数据库迁移脚本 这里 的第 13 行看到它。

以下是包含验证的 Pilot 模型的外观

app/models/pilot.rb

class Pilot < ActiveRecord::Base   belongs_to :ship   attr_accessible :call_sign, :first_name, :last_name, :ship_id   validates :first_name,             :presence => true,
            :length => { :maximum => 50 }

  validates :last_name,
            :presence => true,
            :length => { :maximum => 50 }

  validates :call_sign,
            :presence => true,
            :uniqueness => { :case_sensitive => false },
            :length => { :maximum => 50, :minimum => 5 }
end

好的——现在我们已经完成了验证,让我们看看当我们点击“添加 Ship”按钮然后不输入任何内容就点击“保存”时会发生什么。

Save Ship error message

不算太差。巧合的是,如果我们这样做,我们也会收到同样的错误消息:

  1. 点击主页上的“添加 Ship”按钮创建一个新的 Ship
  2. 点击“新 Ship”页面上的“添加 Pilot”按钮……你知道的……添加一个新的 Pilot。等等。
  3. 点击“添加 Pilot”模态表单上的“添加”按钮。
  4. 我们会在“添加 Ship”表单的“Pilots”部分看到一个新行,并且所有字段都将是空的。

为什么我们不会收到关于空字段 Pilot 的错误?这是我们的 Ship:reject_if 在起作用。ActiveRecord 发现 Pilot 是“空的”,所以它丢弃了该记录。

允许用户在“添加 Pilot”模态表单上点击“添加”按钮,并故意无视我们精心制定的要求,是对我们谦卑的应用程序的侮辱。我告诉你,这不能容忍!好吧——也许有点太严厉了,但你知道我的意思。如果我们不为我们的应用程序辩护,谁会呢?如果我们希望用户为我们提供 Pilot 的数据,我们应该尽力确保他们提供。

我花了一些时间在 Google 上搜索客户端表单验证工具的好解决方案,而且有很多。我选择了 这篇文章,它使用了 jQuery-Form-Validator。实现看起来很简单,而且这篇文章加分,因为它也使用了 Twitter Bootstrap。

jQuery-Form-Validator

添加 jQuery-Form-Validator 非常简单。只需将 jquery-form-validator.min.js 文件放到你的项目中即可。除了 jQuery 本身之外,没有其他依赖项。

使用 jQuery-Form-Validator 也非常简单。你所要做的就是为你想要验证的 element 添加一个 data-validation 属性。你可以访问 GitHub 页面查看所有可用的验证选项(选项非常多)。

你还可以通过添加一个 data-validation-error-msg 属性来定制错误消息。还有许多其他配置选项可用,例如:错误消息的位置、data-* 属性的名称、错误消息的 CSS 类等。你可以在 GitHub 页面或 jQuery-Form-Validator 演示页面 上查看它们。

下面是我们的 Pilot 模态表单的代码,它实现了 jQuery-Form-Validator

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", :data => { :validation => "required validate_max_length length50", "validation-error-msg" => "First Name is required and cannot be longer than 50 characters." }) %>
        </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", :data => { :validation => "required validate_max_length length50", "validation-error-msg" => "Last Name is required and cannot be longer than 50 characters." }) %>
        </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", :data => { :validation => "required validate_length length5-50", "validation-error-msg" => "Call Sign is required and cannot be must be between 5 and 50 characters." }) %>
        </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>

就像这位 可敬的伙计 所做的那样,我们将一些 CSS 添加到我们的 application.css 文件中,以确保我们的错误消息出现在有问题的元素下方。

app/assets/stylesheets/application.css.scss

.jquery_form_error_message{
  display: block;
  margin-top: 3px;
  color: #f00;
}

现在,我们只需要将 jQuery-Form-Validator 连接到我们的“添加 Pilot”模态表单。方法如下:

app/assets/javascripts/ships.js

// Set up the UI/UX for the ships screens.  This object sets up all the functionality we need to:
//  1.  Bind to the "click" event of the "#addButton" on the modal form.
//  2.  Append data from the modal form to the Pilots table.
//  3.  Hide the modal form when the user is done entering data.
//
// If any other events need to be wired up, the init function would be the place to put them.
var pilotFieldsUI = {
    init: function() {
        // Configuration for the jQuery validator plugin:
        // Set the error messages to appear under the element that has the error.  By default, the
        // errors appear in the all-too-familiar bulleted-list.
        // Other configuration options can be seen here:  https://github.com/victorjonsson/jQuery-Form-Validator
        var validationSettings = {
            errorMessagePosition : 'element'
        };

        // Run validation on an input element when it loses focus.
        $('#new-pilot-fields').validateOnBlur();

        $('#addButton').on('click', function(e) {
            // If the form validation on our Pilots modal "form" fails, stop everything and prompt the user
            // to fix the issues.
            var isValid = $('#new-pilot-fields').validate(false, validationSettings);
            if(!isValid) {
                e.stopPropagation();

                return false;
            }

            // This is the code from previous posts...
            formHandler.appendFields();
            formHandler.hideForm();
        });
    }
};

在第 13 行,我们正在构建一个包含 jQuery-Form-Validator 设置的配置对象。我们目前只想设置错误消息显示在 input element 旁边,所以这就是我们正在设置的所有内容。在第 18 行,我们告诉 jQuery-Form-Validator 在我们的任何元素失去焦点时触发。我是否解释得太多了?可能。几乎可以肯定。但现在已经太晚了!

在第 23 行,事情变得稍微有趣一些。由于“添加 Pilot”模态表单不是一个标准的表单(记住,它是在我们的 Ship form 上的一个 <div>),我们的“添加”按钮不是一个 submit。但是,我们仍然会在我们的 #new-pilot-fields 上调用 .validate。如果我们的验证失败,那么就停止一切,让用户修复他们的问题。

现在我们已经连接好了一切,当用户点击“添加”按钮后,效果如下:

Pilot modal form with client-side validation in effect.

作为一项附加福利,当我们将新的 Pilot 添加到 Ship form 的“Pilots”部分时,验证功能也会随之生效。当用户成功添加 Pilot,但随后在点击保存之前删除了呼号时,效果如下:

Validation on the Pilot fields works after adding to the Pilots table.

注意事项

上述技巧并非 100% 万无一失——记住,我们在提交 Ship form 时并没有调用 jQuery-Form-Validator 的 validate() 方法。实现起来很容易,所以我就留给亲爱的读者作为练习了。但是,我们确实受到我们之前在模型中实现的服务器端验证的保护。看?

Pilot validation still happens on the server-side.

它并不完美,但总比没有好。我们将在以后的文章中尝试清理这个问题。

结论

女士们先生们,我们确实越来越接近那个排气口了!我们的星际战斗机识别指南的护盾已经通过一些客户端验证得到了加强。这些护盾升级将有助于抵挡用户无疑会向我们投掷的攻击。我们勇敢的英雄们还没有完全脱离危险——我们还可以添加一些额外的验证(总是有的,不是吗?),但当我们飞行员接近目标时,我们会处理它。

参考

以下是一些对我的这篇文章非常有帮助的文章/工具:


© . All rights reserved.