http://iany.me/2013/01/rails-compound-input/
介绍 Rails 中两种实现组合输入方法。
这源于这样一个需要,表单中需要输入时间,对应的字段是 datetime
,为了方便输入,
希望把日期部分做成文本框,然后用 JavaScript 的 date picker 来选择。时间部份可
以要求用户手动输入,或者也使用 JavaScript 的 time picker。把下面的例子扩展,也
可以实现时间部分使用下拉菜单来输入。
两种方法分别使用了 composed_of
和 fields_for
。 composed_of
和
datetime_select
类似,使用了 assign_multiparameter_attributes
这个方法。而
fields_for
则是伪装成 association。
两种方法的示例可以在 doitian/rails-compound-input-demo 中找到。
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
在 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
在 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>
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
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
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>
[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"