<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Form</title><link>https://blog.iany.me/tags/form/</link><description>Recent content in Form «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Sat, 19 Jan 2013 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iany.me/tags/form/index.xml" rel="self" type="application/rss+xml"/><item><title>Rails Compound Input</title><link>https://blog.iany.me/2013/01/rails-compound-input/</link><pubDate>Sat, 19 Jan 2013 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2013/01/rails-compound-input/</guid><description>&lt;p&gt;When I implement
&lt;a href="https://github.com/19wu/19wu/pull/160"&gt;time input feature&lt;/a&gt; for
&lt;a href="https://github.com/saberma/19wu"&gt;19wu&lt;/a&gt; (an open source ticket sale system), I
want to split the datetime into date and time parts, so JavaScript date picker
and time picker can be used. This post introduces two methods I found.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Compound datetime input" class="kg-image" loading="lazy" src="https://blog.iany.me/2013/01/rails-compound-input/compound_datetime_input_hu_294b76325e647193.png" /&gt;
&lt;figcaption &gt;Compound datetime input&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;composed_of&lt;/code&gt; utilizes &lt;code&gt;assign_multiparameter_attributes&lt;/code&gt; trick like
&lt;code&gt;datetime_select&lt;/code&gt;, and &lt;code&gt;fields_for&lt;/code&gt; mocks an association.&lt;/p&gt;
&lt;p&gt;The github repository &lt;a href="https://github.com/doitian/rails-compound-input-demo"&gt;doitian/rails-compound-input-demo&lt;/a&gt; contains demos for both methods.&lt;/p&gt;
&lt;h2 id="composed_of"&gt;composed_of&lt;/h2&gt;
&lt;p&gt;Rails has built-in compound inputs &lt;code&gt;date_select&lt;/code&gt;, &lt;code&gt;time_select&lt;/code&gt; and
&lt;code&gt;datetime_select&lt;/code&gt;. They use trick that if parameter name has parentheses, they
will be used in the attribute class constructor (see
&lt;a href="http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_multiparameter_attributes" title="ActiveRecord::AttributeAssignment#assign_multiparameter_attributes"&gt;assign_multiparameter_attributes&lt;/a&gt;). However, datetime attribute is of type
DateTime, which accepts 6 parameters year, month, day, hour, minute and
second.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html#method-i-composed_of" title="ActiveRecord::Aggregations.composed_of"&gt;composed_of&lt;/a&gt; can be used here to represent the datetime attribute using
value object, which class constructor accepts date and time strings. See
&lt;code&gt;CompoundDatetime#initialize&lt;/code&gt; below.&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/composed_of/app/models/compound_datetime.rb"&gt;compound_datetime.rb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then setup the mapping in model &lt;code&gt;Event&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/composed_of/app/models/event.rb"&gt;event.rb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;class Event &amp;lt; ActiveRecord::Base
attr_accessible :beginning_time, :title
attr_accessible :compound_beginning_time
composed_of :compound_beginning_time, {
:class_name =&amp;gt; 'CompoundDatetime',
:mapping =&amp;gt; [ %w(beginning_time datetime) ],
:converter =&amp;gt; Proc.new { |datetime| CompoundDatetime.from_datetime(datetime) }
}
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form view just needs set correct name:&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/composed_of/app/views/events/_form.html.erb"&gt;events/_form.html.erb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&amp;quot;field&amp;quot;&amp;gt;
&amp;lt;%= f.label :compound_beginning_time, 'Begining Time' %&amp;gt;&amp;lt;BR /&amp;gt;
&amp;lt;%= text_field_tag 'event[compound_beginning_time(1s)]', @event.compound_beginning_time.date, :placeholder =&amp;gt; 'yyyy-mm-dd' %&amp;gt;
&amp;lt;%= text_field_tag 'event[compound_beginning_time(2s)]', @event.compound_beginning_time.time, :placeholder =&amp;gt; 'HH:MM' %&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="fields_for"&gt;fields_for&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fields_for&lt;/code&gt; is usually used to embed associations in form. However, all it
required was a method to return the named attribute, and then a &lt;code&gt;&amp;lt;field&amp;gt;_attributes=&lt;/code&gt;
writer to interpret the hash on the other side. See the section &amp;ldquo;Nested Attributes Examples&amp;rdquo; in the
&lt;a href="https://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for"&gt;&lt;code&gt;fields_for&lt;/code&gt; API document&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First create &lt;code&gt;CompoundDatetime&lt;/code&gt; class which exposes date and time
fields. &lt;code&gt;assign_attributes&lt;/code&gt; handles the hash params passed from form. Method
&lt;code&gt;persisted?&lt;/code&gt; is required to quiet &lt;code&gt;NoMethodError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/fields_for/app/models/compound_datetime.rb"&gt;compound_datetime.rb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;class CompoundDatetime
attr_accessor :datetime
def initialize(datetime)
@datetime = datetime
end
# accepts hash like:
#
# {
# 'date' =&amp;gt; '2012-12-20',
# 'time' =&amp;gt; '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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model just delegates the named attribute and &lt;code&gt;&amp;lt;field&amp;gt;_attributes=&lt;/code&gt; method to &lt;code&gt;CompoundDatetime&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/fields_for/app/models/event.rb"&gt;event.rb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;class Event &amp;lt; 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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form view uses &lt;code&gt;fields_for&lt;/code&gt; helper to nest fields of &lt;code&gt;compound_begining_time&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/rails-compound-input-demo/blob/master/fields_for/app/views/events/_form.html.erb"&gt;events/_form.html.erb&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&amp;quot;field&amp;quot;&amp;gt;
&amp;lt;%= f.label :beginning_time %&amp;gt;&amp;lt;br /&amp;gt;
&amp;lt;%= f.fields_for :compound_beginning_time do |fields| %&amp;gt;
&amp;lt;%= fields.text_field :date, :placeholder =&amp;gt; 'yyyy-mm-dd' %&amp;gt;
&amp;lt;%= fields.text_field :time, :placeholder =&amp;gt; 'HH:MM' %&amp;gt;
&amp;lt;% end %&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="reference"&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.alexrothenberg.com/2009/05/21/how-to-use-dates-in-rails-when-your.html"&gt;Alex Rothenberg - How to use dates in Rails when your database stores a string&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for"&gt;&lt;code&gt;fields_for&lt;/code&gt; API document&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="http://guides.rubyonrails.org/form_helpers.html"&gt;Ruby on Rails Guides: Rails Form helpers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_multiparameter_attributes" title="ActiveRecord::AttributeAssignment#assign_multiparameter_attributes"&gt;assign_multiparameter_attributes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html#method-i-composed_of" title="ActiveRecord::Aggregations.composed_of"&gt;composed_of&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/form/">Form</category><category domain="https://blog.iany.me/tags/frontend/">Frontend</category><category domain="https://blog.iany.me/tags/rails/">Rails</category></item></channel></rss>