homeASCIIcasts

32: Time in Text Field 

(view original Railscast)

Other translations: It

The tasks index page.

Above is an application that displays a list of tasks. Clicking on the edit link for a task takes us to the edit page for that task. The edit page uses the datetime_select helper method to render the tasks as a list of drop down lists.

The edit page for a task.

Changing five drop downs isn’t the simplest way to update the due date. It would be easier for the users if they could enter the date and time in a single text field. Our application could then parse what they entered and store it back in the database.

Replacing the drop-downs with a text field will mean that we have a field on the form that doesn’t directly map on to a field in the database. We’ll have to create a virtual attribute in the Task model to handle the new field. Before we do that we’ll update the view code and replace our drop-downs with a text field called due_at_string.

<h1>Edit Task</h1>
<% form_for @task do |form| %>
<ol class="formList">
  <li>
    <%= form.label :name, "Name" %>
    <%= form.text_field :name %>
  </li>
  <li>
    <%= form.label :due_at_string, "Due at" %>
    <%= form.text_field :due_at_string %>
  </li>
  <li>
    <%= submit_tag "Edit task" %>
  </li>
</ol>
<% end %>

The edit view code with our new text field in it.

Now that we’ve updated the form we’ll have to create getter and setter methods for the virtual attribute in the model. Virtual attributes were covered back in episode 16; have a look there if you need some more information about them.

class Task < ActiveRecord::Base
  belongs_to :project
  validates_presence_of :name

  def due_at_string
    due_at.to_s(:db)
  end

  def due_at_string=(due_at_str)
    self.due_at = Time.parse(due_at_str)
  end
end

The getter method gets the task’s due_at date from the database and returns a string representation of it. The setter method could be a little tricky as it will have to take whatever input the user provides and convert it into a date and time. Thankfully, the Time object provides a parse method that will try to convert the value entered into a valid date.

If we reload our page now we’ll see that the drop-downs have been replaced with a textbox showing the due at date for our task. The Time.parse method is fairly clever at parsing the text we pass it, so we can enter, say, March 1st at 8:00PM and it will be able to convert it.

Sometimes though, we need more flexibility than Time.parse can provide. The Chronic gem, which can be installed with sudo gem install chronic is useful if we need to be able to accept more types of date and time. To use it we just add require 'chronic' to the top of the Task class and replace Time.parse in our setter method with Chronic.parse so that it looks like this.

  def due_at_string=(due_at_str)
    self.due_at = Chronic.parse(due_at_str)
  end

One of the advantages of Chronic is that it can handle relative dates, so we can pass "tomorrow", "Monday" or even "next Tuesday at 8pm" and it will parse them correctly.

Handling Exceptions

Chronic will return nil if we pass it a string that it cannot parse, which we can check for, but Time.parse will raise an ArgumentError exception. Obviously we’ll need to handle this so we’ll add a rescue block to the setter method. We’ll add an instance variable to the class that will be set to true if the date entered causes an exception to be raised and then add a validate method to the class that will add an error to the task’s errors if the time passed could not be parsed.

def due_at_string=(due_at_str)
  self.due_at = Time.parse(due_at_str)
rescue ArgumentError
  @due_at_invalid = true
end

def validate
  errors.add(:due_at, "is invalid") if @due_at_invalid
end

If we try to pass an invalid date now, the error will be caught and added to the model’s list of errors. It’s worth noting that Time.parse will replace missing or completely invalid parts of the string passed with the equivalent part from Time.now, so if we pass a completely invalid value like "Hello, world!" the due date will be set to the current time. An exception will only be raised if an invalid date such as "32-12-2009" is passed.

The edit page will now show an error if an invalid date is passed.

We now have a much better way of inputting dates and times into Rails applications. Whether we choose to use the Chronic gem or the built-in time parsing methods we can handle a wide range of date formats from user input.