32: Time in Text Field
(view original Railscast)
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.
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.
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.