Rails date format in a text_field

后端 未结 14 2165
傲寒
傲寒 2020-12-13 06:05

I have a rails form that displays a date in a text_field:

<%= form.text_field :check_in_date  %>

The date is rendered as yyyy-m

相关标签:
14条回答
  • 2020-12-13 06:30

    Expanding Kamil's answer a bit: add the following method to application_helper.rb:

    def date_mdY(date)
      if date.nil?
        ""
      else
        date.strftime("%m-%d-%Y")
      end
    end
    

    Then you can modify Kamil's answer slightly:

    <%= f.text_field :some_date, :value => date_mdY(@model_instance.some_date) %>
    

    Then you can use this helper method in any other view.

    I'm using the jQuery datepicker and the date needed to be in a particular format in order for the default date to be set correctly. Thanks for the answer Kamil!

    0 讨论(0)
  • 2020-12-13 06:31

    There are two parts to making this work, and a third part to make it all work well. If you just want to copy paste, go ahead and scroll to the end.

    Part 1: Getting Date to format itself as desired

    There are a bunch of core extensions to Date in ActiveSupport, but none of them create a new date class. If you're working with Date, which is what Rails returns when you have a date column in your database, the method converting that date into a string is Date#to_formatted_s. The ActiveSupport extensions add this method, and they alias the to_s method to it, so when you call Date#to_s (probably implicitly), you get Date.to_formatted_s.

    There are two ways to change the format used by to_formatted_s:

    1. Pass the name of the format you would like used (this arg will be used to lookup the format in Date::DATE_FORMATS).
    2. Change the format that :default maps to. Since to_formatted_s sets a default value of :default, when you call Date#to_s without passing an argument you get format specified by Date::DATE_FORMATS[:default]. You can override the default on an application-wide basis like this:

      Date::DATE_FORMATS[:default] = "%m/%d/%Y"
      

    I have this set in an initializer like config/initializers/date_formats.rb. It's crude and doesn't support changing with your localizations, but gets Date#to_s to return the desired format without any monkey patching or explicitly converting your dates to strings and passing in an argument.

    Part 2: Getting user-inputted dates converted to Date objects

    By default, your ActiveRecord instance will cast date columns using good ol' ISO 8601 that looks like this: yyyy-mm-dd.

    If it doesn't match that format exactly (e.g., it's slash delimited), it falls back to using Ruby's Date._parse, which is a little more lenient, but still expects to see days, months, then years: dd/mm/yyyy.

    To get Rails to parse the dates in the format you're collecting them, you just need to replace the cast_value method with one of your own. You can monkeypatch ActiveRecord::Types::Date, but it's not much harder to roll your own type inheriting from it.

    I like using Chronic to parse date strings from user input because it puts up with more shit, like "Tomorrow", or "Jan 1 99", or even bonkers input like "5/6-99" (May 6, 1999). Since Chronic returns an instance of Time and we're overriding the supplied cast_value method, we need 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 ActiveRecord to use EasyDate instead of ActiveRecord::Type::Date:

    class Pony < ApplicationRecord
      attribute :birth_date, EasyDate.new
    end
    

    That works, but if you're like me, you want to take on a bunch more work right now so you can save 3 seconds in the future. What if we could tell ActiveRecord to always use EasyDate for columns that are just dates? We can.

    Part 3: Make Rails handle this automatically

    ActiveRecord gives you all sorts of information about your model that it uses to handle all of the “magic” behind the scenes. One of the methods it gives you us attribute_types. If you run that in your console, you get back vomit — it's kind of scary, and you just go “oh nevermind”. If you dig into that vomit like some sort of weirdo, though, it's just a hash that looks like this:

    { column_name: instance_of_a_type_object, ... }
    

    Not scary stuff, but since we're starting to poke at internals, the inspected output of these instance_of_a_type_object can end up looking obtuse and “closed”. Like this:

    #<ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Money:0x007ff974d62688
    

    Thankfully, we really only care about one: ActiveRecord::Types::Date, so we can narrow it down:

    date_attributes = attribute_types.select { |_, type| ActiveRecord::Type::Date === type }
    

    That gives us a list of the attributes that we want to upcycle to use EasyDate. Now we just need to tell ActiveRecord to use EasyDate on those attributes like we did above:

    date_attributes.each do |attr_name, _type|
      attribute attr_name, EasyDate.new
    end
    

    And that basically does it, but we need to glue it all together. If you're using ApplicationRecord you can drop this right into it and 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 “pollute” ApplicationRecord, you can do like I did and create a module and extend ApplicationRecord with it. If you're not using ApplicationRecord, you'll need to create the module 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 the how and just want this to work, you can:

    1. Add gem "chronic" to your Gemfile and bundle install
    2. Make sure your models inherit from ApplicationRecord
    3. Copy the code below a file at config/initializers/date_formatting.rb
    4. Restart
    5. Everything should now Just Work™

    Here's 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
    
    0 讨论(0)
  • 2020-12-13 06:35

    Simple one time solution is to use :value option within text_field call instead of adding something to ActiveRecord::Base or CoreExtensions.

    For example:

    <%= f.text_field :some_date, value: @model_instance.some_date.strftime("%d-%m-%Y") %>
    
    0 讨论(0)
  • 2020-12-13 06:35

    If you can set the value manually you can use created_at.strftime("%m-%d-%Y") http://snippets.dzone.com/tag/strftime .

    0 讨论(0)
  • 2020-12-13 06:38

    For people interested to format in a specific location and not all over the project, several answers advised to check if null and I want to add that using try with strftime will help to make 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 %>
    
    0 讨论(0)
  • 2020-12-13 06:41

    My preferred way of handling this is with virtual attributes. There's a short RailsCast on the subject (3m), but the upshot is that virtual attributes will allow you to use a non-standard format when displaying model attributes in a form, or vice versa (i.e., saving form data to a model).

    Basically, rather than creating a form field for the check_in_date attribute, you can define a “virtual attribute” on the 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 correspond to check_in_date_string rather than check_in_date:

    <%= f.text_field :check_in_date_string %>
    

    That's it! No need to explicitly specify a value option, and when this form is submitted, the model will be updated appropriately.

    0 讨论(0)
提交回复
热议问题