Rails Rails 两种创建组合输入的方法

doitian · January 19, 2013 · Last by doitian replied at January 20, 2013 · 5823 hits

http://iany.me/2013/01/rails-compound-input/

介绍 Rails 中两种实现组合输入方法。

这源于这样一个需要,表单中需要输入时间,对应的字段是 datetime,为了方便输入, 希望把日期部分做成文本框,然后用 JavaScript 的 date picker 来选择。时间部份可 以要求用户手动输入,或者也使用 JavaScript 的 time picker。把下面的例子扩展,也 可以实现时间部分使用下拉菜单来输入。

两种方法分别使用了 composed_offields_forcomposed_ofdatetime_select 类似,使用了 assign_multiparameter_attributes 这个方法。而 fields_for 则是伪装成 association。

两种方法的示例可以在 doitian/rails-compound-input-demo 中找到。

composed_of

Rails 本身就有 date_select, time_select and datetime_select 这些组合输入控件。它们都利用了 [assign_multiparameter_attributes][] 这个特性:如果参数名最后带有小括号,会按 照括号中的位置对应到 constructor 中。但是 datetime 对应的类是 DateTime,它的 构造函数接受的是 6 个参数,年、月、日、时、分、秒。

这里可以使用 [composed_of][] 来把 attribute 转换成 value object, 并且构造函数使用 日期 和 时间 两个字符串类型参数。见下面 CompoundDatetime#initialize

class CompoundDatetime
  def self.from_datetime(datetime)
    new.tap do |result|
      result.datetime = datetime
    end
  end

  attr_accessor :datetime

  # Accepts date and time string. The form just need to submit params
  #
  #   - compound_beginning_time(1s) for date
  #   - compound_beginning_time(2s) for time
  def initialize(date = nil, time = nil)
    if date.present?
      @datetime = Time.zone.parse([date.presence, time.presence || ''].join(' '))
    end
  end

  def date
    @datetime.strftime('%Y-%m-%d') if @datetime
  end

  def time
    @datetime.strftime('%H:%M') if @datetime
  end
end

compound_datetime.rb

在 model 中使用 composed_of 建立映射:

class Event < ActiveRecord::Base
  attr_accessible :beginning_time, :title
  attr_accessible :compound_beginning_time

  composed_of :compound_beginning_time, {
    :class_name => 'CompoundDatetime',
    :mapping => [ %w(beginning_time datetime) ],
    :converter => Proc.new { |datetime| CompoundDatetime.from_datetime(datetime) }
  }
end

event.rb

在 form view 中只需要正确设置参数名。

<div class="field">
  <%= f.label :compound_beginning_time, 'Begining Time' %><BR />
  <%= text_field_tag 'event[compound_beginning_time(1s)]', @event.compound_beginning_time.date, :placeholder => 'yyyy-mm-dd' %>
  <%= text_field_tag 'event[compound_beginning_time(2s)]', @event.compound_beginning_time.time, :placeholder => 'HH:MM' %>
</div>

events/_form.html.erb

fields_for

fields_for 一般用来在 form 中嵌入 associations。不过

它所需要的只是一个 getter 方法和 <field>_attributes= 的 setter 方法。 Compound Attributes and fields_for in Rails | Wondible

首先还是创建 CompoundDatetime,它需要有 date 和 time 两个 attributes。 assign_attributes 用来处理 form 传来的 params hash。方法 persisted? 是为了消除 NoMethodError

class CompoundDatetime
  attr_accessor :datetime

  def initialize(datetime)
    @datetime = datetime
  end

  # accepts hash like:
  #
  #     {
  #       'date' => '2012-12-20',
  #       'time' => '20:30'
  #     }
  def assign_attributes(hash)
    if hash[:date].present?
      @datetime = Time.zone.parse([hash[:date].presence, hash[:time].presence || ''].join(' '))
    end
    self
  end

  def date
    @datetime.strftime('%Y-%m-%d') if @datetime
  end

  def time
    @datetime.strftime('%H:%M') if @datetime
  end

  def persisted?; false; end
end

compound_datetime.rb

Model 只需要代理给 CompoundDatetime.

class Event < ActiveRecord::Base
  attr_accessible :beginning_time, :title

  attr_accessible :compound_beginning_time_attributes

  def compound_beginning_time
    CompoundDatetime.new(beginning_time)
  end

  def compound_beginning_time_attributes=(attributes)
    self.beginning_time = compound_beginning_time.assign_attributes(attributes).datetime
  end
end

event.rb

Form view 中使用 fields_for 来嵌入 compound_begining_time 的属性

<div class="field">
  <%= f.label :beginning_time %><br />
  <%= f.fields_for :compound_beginning_time do |fields| %>
    <%= fields.text_field :date, :placeholder => 'yyyy-mm-dd' %>
    <%= fields.text_field :time, :placeholder => 'HH:MM' %>
  <% end %>
</div>

events/_form.html.erb

参考

[assign_multiparameter_attributes]: http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_multiparameter_attributes "ActiveRecord::AttributeAssignment#assign_multiparameter_attributes" [composed_of]: http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html#method-i-composed_of "ActiveRecord::Aggregations.composed_of"

相当精彩,刚看到就在这里看到了。学习。

考虑封装成 gem 不?

#2 楼 @flyerhzm CompoundDatetime 装备抽出来。这两种方法本身代码量不多,感觉不是很有必要抽取成 gem

You need to Sign in before reply, if you don't have an account, please Sign up first.