[ACCEPTED]-Rails date format in a text_field-ruby-on-rails
Simple one time solution is to use :value
option 2 within text_field
call instead of adding something 1 to ActiveRecord::Base
or CoreExtensions
.
For example:
<%= f.text_field :some_date, value: @model_instance.some_date.strftime("%d-%m-%Y") %>
I added these to my initializers/time_formats.rb
and it works great:
# Default format for displaying dates and times
Date::DATE_FORMATS[:default] = "%m/%d/%Y"
Time::DATE_FORMATS[:default] = "%m/%d/%Y"
Using 1 Ruby 1.9.3 and Rails 3.2.x
I spent some time looking in the code, and 32 changing the DATE_FORMAT won't work because 31 Rails never asks for the date format. Felix's 30 solution of setting :value does work, but 29 feels cumbersome: why should I have to set 28 the value of date-specific textfields by 27 hand? So this is a codesmell, and I wanted 26 something better.
Here's my short solution, but 25 then I'll explain it a bit because I'm hoping 24 somebody else will write something better:
MyModel < ActiveRecord::Base
# ...
self.columns.each do |column|
if column.type == :date
define_method "#{column.name}_before_type_cast" do
self.send(column.name).to_s
end
end
end
end
Somewhat 23 longer explanation:
When a textfield is 22 setting the value attribute, it'll first 21 look in the options hash (which is why Felix's 20 solution works), but if it's not there, it'll 19 call form.object.check_in_date_before_type_cast
, which (after some method_missing 18 magic) will call form.object.attributes_before_type_cast['check_in_date']
, which will look up 'check_in_date' in 17 the @attributes hash inside form.object. My 16 guess is that the @attributes hash is getting 15 the direct MySQL string representation (which 14 follows the 'yyyy-mm-dd' format) before 13 it gets wrapped in a Ruby object, which 12 is why simply setting the DATE_FORMAT doesn't 11 work by itself. So the dynamic method creation 10 above creates methods for all date objects 9 that actually perform the typecast, allowing 8 you to format the date using ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[:default]
.
This feels 7 a little dirty because the whole purpose 6 of '_before_type_cast' is to avoid a typecast, and 5 this is one of those "thar be dragons" monkeypatches. But 4 still, from what I can tell, it feels like 3 the best of what's around. Maybe somebody 2 else can do a little more digging and find 1 a better solution?
Expanding Kamil's answer a bit: add the 7 following method to application_helper.rb:
def date_mdY(date)
if date.nil?
""
else
date.strftime("%m-%d-%Y")
end
end
Then 6 you can modify Kamil's answer slightly:
<%= f.text_field :some_date, :value => date_mdY(@model_instance.some_date) %>
Then 5 you can use this helper method in any other 4 view.
I'm using the jQuery datepicker and 3 the date needed to be in a particular format 2 in order for the default date to be set 1 correctly. Thanks for the answer Kamil!
config/initializers/date_format.rb
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[:default] = '%m/%d/%Y'
view
<%= form.text_field :check_in_date, value: model.check_in_date.to_s %>
0
This one use location, in your form:
<%= f.text_field :date3, value: ( l @model.date3 if @model.date3? ) %>
In locations 1 en.yml (or es.yml)
en:
date:
formats:
default: "%d/%m/%Y"
There are two parts to making this work, and 73 a third part to make it all work well. If 72 you just want to copy paste, go ahead and 71 scroll to the end.
Part 1: Getting Date
to format itself as desired
There are a bunch of 70 core extensions to Date in ActiveSupport, but none of them create a new date class. If 69 you're working with Date
, which is what Rails 68 returns when you have a date
column in your 67 database, the method converting that date 66 into a string is Date#to_formatted_s
. The ActiveSupport
extensions add this 65 method, and they alias the to_s
method to it, so 64 when you call Date#to_s
(probably implicitly), you 63 get Date.to_formatted_s
.
There are two ways to change the format 62 used by to_formatted_s
:
- Pass the name of the format you would like used (this arg will be used to lookup the format in
Date::DATE_FORMATS
). Change the format that
:default
maps to. Since 61to_formatted_s
sets a default value of:default
, when you call 60Date#to_s
without passing an argument you get format 59 specified byDate::DATE_FORMATS[:default]
. You can override the default 58 on an application-wide basis like this:Date::DATE_FORMATS[:default] = "%m/%d/%Y"
I 57 have this set in an initializer like config/initializers/date_formats.rb
. It's 56 crude and doesn't support changing with 55 your localizations, but gets Date#to_s
to return 54 the desired format without any monkey patching 53 or explicitly converting your dates to strings 52 and passing in an argument.
Part 2: Getting user-inputted dates converted to Date objects
By default, your 51 ActiveRecord
instance will cast date columns using good ol' ISO 8601 50 that looks like this: yyyy-mm-dd
.
If it doesn't match 49 that format exactly (e.g., it's slash delimited), it 48 falls back to using Ruby's Date._parse, which is a 47 little more lenient, but still expects to 46 see days, months, then years: dd/mm/yyyy
.
To get Rails 45 to parse the dates in the format you're 44 collecting them, you just need to replace 43 the cast_value
method with one of your own. You can monkeypatch 42 ActiveRecord::Types::Date
, but it's not much harder to roll your 41 own type inheriting from it.
I like using 40 Chronic to parse date strings from user input because 39 it puts up with more shit, like "Tomorrow", or 38 "Jan 1 99", or even bonkers input 37 like "5/6-99" (May 6, 1999). Since 36 Chronic returns an instance of Time
and we're 35 overriding the supplied cast_value
method, we need 34 to call to_date
on it.
class EasyDate < ActiveRecord::Type::Date
def cast_value(value)
default = super
parsed = Chronic.parse(value)&.to_date if value.is_a?(String)
parsed || default
end
end
Now we just need to tell 33 ActiveRecord to use EasyDate
instead of ActiveRecord::Type::Date
:
class Pony < ApplicationRecord
attribute :birth_date, EasyDate.new
end
That works, but 32 if you're like me, you want to take on a 31 bunch more work right now so you can save 30 3 seconds in the future. What if we could 29 tell ActiveRecord to always use EasyDate 28 for columns that are just dates? We can.
Part 3: Make Rails handle this automatically
ActiveRecord 27 gives you all sorts of information about 26 your model that it uses to handle all of 25 the “magic” behind the scenes. One of the 24 methods it gives you us attribute_types
. If you run that 23 in your console, you get back vomit — it's 22 kind of scary, and you just go “oh nevermind”. If 21 you dig into that vomit like some sort of 20 weirdo, though, it's just a hash that looks 19 like this:
{ column_name: instance_of_a_type_object, ... }
Not scary stuff, but since we're 18 starting to poke at internals, the inspected 17 output of these instance_of_a_type_object
can end up looking obtuse 16 and “closed”. Like this:
#<ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Money:0x007ff974d62688
Thankfully, we 15 really only care about one: ActiveRecord::Types::Date
, so we can 14 narrow it down:
date_attributes = attribute_types.select { |_, type| ActiveRecord::Type::Date === type }
That gives us a list of the 13 attributes that we want to upcycle to use 12 EasyDate
. Now we just need to tell ActiveRecord 11 to use EasyDate on those attributes like 10 we did above:
date_attributes.each do |attr_name, _type|
attribute attr_name, EasyDate.new
end
And that basically does it, but 9 we need to glue it all together. If you're 8 using ApplicationRecord
you can drop this right into it and 7 be off to the races:
def inherited(klass)
super
klass.attribute_types.select { |_, type| ActiveRecord::Type::Date === type }.each do |name, _type|
klass.attribute name, EasyDate.new
end
end
If you don't want to 6 “pollute” ApplicationRecord
, you can do like I did and create 5 a module and extend ApplicationRecord
with it. If you're 4 not using ApplicationRecord
, you'll need to create the module 3 and extend ActiveRecord::Base
.
module FixDateFormatting
# the above `inherited` method here
end
# Either this...
class ApplicationRecord < ActiveRecord::Base
extend FixDateFormatting
end
# ...or this:
ActiveRecord::Base.extend(FixDateFormatting)
TL;DR — What to copy paste
If you aren't concerned with 2 the how and just want this to work, you can:
- Add
gem "chronic"
to your Gemfile andbundle install
- Make sure your models inherit from
ApplicationRecord
- Copy the code below a file at
config/initializers/date_formatting.rb
- Restart
- Everything should now Just Work™
Here's 1 the code to copy:
Date::DATE_FORMATS[:default] = "%m/%d/%Y"
module FixDateFormatting
class EasyDate < ActiveRecord::Type::Date
def cast_value(value)
default = super
parsed = Chronic.parse(value)&.to_date if value.is_a?(String)
parsed || default
end
end
def inherited(subclass)
super
date_attributes = subclass.attribute_types.select { |_, type| ActiveRecord::Type::Date === type }
date_attributes.each do |name, _type|
subclass.attribute name, EasyDate.new
end
end
end
Rails.application.config.to_prepare do
ApplicationRecord.extend(FixDateFormatting)
end
My preferred way of handling this is with 12 virtual attributes. There's a short RailsCast on the subject (3m), but the upshot 11 is that virtual attributes will allow you 10 to use a non-standard format when displaying 9 model attributes in a form, or vice versa 8 (i.e., saving form data to a model).
Basically, rather 7 than creating a form field for the check_in_date
attribute, you 6 can define a “virtual attribute” on the 5 model:
class Reservation
def check_in_date_string
check_in_date.strftime('%m-%d-%Y')
end
def check_in_date_string=(date_string)
self.check_in_date = Date.strptime(date_string, '%m-%d-%Y')
end
end
Now, you can create form fields that 4 correspond to check_in_date_string
rather than check_in_date
:
<%= f.text_field :check_in_date_string %>
That's it! No 3 need to explicitly specify a value
option, and 2 when this form is submitted, the model will 1 be updated appropriately.
Go to your environment.rb file and add the 2 following:
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(
:default => '%m-%d-%Y' )
Check the official documentation if you 1 feel like reading more :)
So I did something like this in the model.
def start_date
super.strftime "%Y/%m/%d %H:%M"
end
Say 4 you have a column called start_date
in model, and 3 want to display another format in the form, just 2 overwrite its value in model.
And nothing 1 needs to be changed in the form.
<%= f.text_field :start_date, class: 'form-control' %>
For people interested to format in a specific 4 location and not all over the project, several 3 answers advised to check if null and I want 2 to add that using try with strftime will help to make 1 it shorter.
<%= f.fields_for :assets_depreciations do |asset| %>
<tr>
<td>
<%= asset.text_field :depreciation_date, value: asset.object.depreciation_date.try(:strftime, "%d-%m-%Y") %>
</td>
</tr>
<% end %>
If you can set the value manually you can 1 use created_at.strftime("%m-%d-%Y") http://snippets.dzone.com/tag/strftime .
I had similar problems with time attributes/fields. So 14 one can follow this:
http://railscasts.com/episodes/32-time-in-text-field
And it works pretty 13 well.
But I dug into and found another interesting 12 solution. Kind of a ugly monkeypatch, but 11 in some cases it could be more useful that 10 the one from railscasts.
So I have a model 9 with a column named time
(ofc it has a time 8 type). Here is my solution:
after_initialize :init
private
def init
unless time.nil?
@attributes['time'] = I18n.l(time, format: :short)
end
end
As you can see 7 I format the variable which going to be 6 returned by time_before_type_cast
method. So I have properly 5 formatted time in my text input (rendered 4 by FormBuilder
), but if user puts something wrong like 3 10;23
I still have this and in next request it 2 will be rendered by FormBuilder::text_field
. So user has an opportunity 1 to fix his miserable mistake.
A slight improvement of Tom Rossis answer 2 is to use the I18n.localize
method, so the default will 1 always be in the current users locale:
initializers/date_time_i18n_formatter.rb
Date::DATE_FORMATS[:default] = ->(date) { I18n.localize date, format: :default }
Time::DATE_FORMATS[:default] = ->(time) { I18n.localize time, format: :default }
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.