32: Time in Text Field
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
<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.
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.