Adding Custom Form API Elements to Drupal
In a previous post, we took a look at how to collect a customized date in Drupal. You may have noticed that there are warnings that the Day field are undefined, and that Drupal returns a form error saying the 'specified date is invalid'. This is because we are not passing a day part. In most cases, you would, but using the previous example again of submitting a credit card expiration date, you don't need it. So, how can we solve this problem?
Drupal provides an easy way to define your own form elements through the use of hook_element_info. With this function, we can quickly define new Form API elements to use- in this case, we want to create a custom date field, modeled after the core date field. While we're at it, lets also create two more fields that are related- credit card number, and credit card CVV code fields.
The code to define your elements is simple:
function creditfield_element_info() {
$types['creditfield_cardnumber'] = array(
'#input' => TRUE,
'#element_validate' => array('creditfield_cardnumber_validate'),
'#autocomplete_path' => FALSE,
'#process' => array('form_process_creditfield'),
'#theme' => 'textfield',
'#theme_wrappers' => array('form_element'),
'#maxlength' => 16,
);
$types['creditfield_date'] = array(
'#input' => TRUE,
'#element_validate' => array('creditfield_date_validate'),
'#process' => array('form_process_creditfield_date'),
'#theme' => 'date',
'#theme_wrappers' => array('form_element'),
);
$types['creditfield_cvv'] = array(
'#input' => TRUE,
'#element_validate' => array('creditfield_cvv_validate'),
'#autocomplete_path' => FALSE,
'#process' => array('form_process_creditfield'),
'#theme' => 'textfield',
'#theme_wrappers' => array('form_element'),
'#maxlength' => 4,
);
return $types;
}Breaking it down, we are simply defining 3 elements, validators, and processors. Since the date field is the complicated one, lets look at its #process callback, form_process_creditfield_date. This function determines how the field is constructed.
function form_process_creditfield_date($element) {
// Default to current date
if (empty($element['#value'])) {
$element['#value'] = array(
'month' => format_date(REQUEST_TIME, 'custom', 'n'),
'year' => format_date(REQUEST_TIME, 'custom', 'Y'),
);
}
$element['#tree'] = TRUE;
// Determine the order of month & year in the site's chosen date format.
$format = variable_get('date_format_short', 'm/Y');
$sort = array();
$sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
$sort['year'] = strpos($format, 'Y');
asort($sort);
$order = array_keys($sort);
// Output multi-selector for date.
foreach ($order as $type) {
switch ($type) {
case 'month':
$options = drupal_map_assoc(range(1, 12), 'map_month');
$title = t('Month');
break;
case 'year':
$options = drupal_map_assoc(range(date('Y', time()), date('Y', time()) + 10));
$title = t('Year');
break;
}
$element[$type] = array(
'#type' => 'select',
'#title' => $title,
'#title_display' => 'invisible',
'#value' => $element['#value'][$type],
'#attributes' => $element['#attributes'],
'#options' => $options,
);
}
return $element;
}I modeled this after the Drupal core form_process_date function, except we have removed the 'day' field logic, as well as changed the year options to be the current year plus 10 years, instead of the default of 1950-2050 range. When you call this field type in your form, you get a date field with just the month and year. Adding in the validation, there won't be too much more you have to do in your form:
function creditfield_date_validate($element) {
if ($element['#value']['year'] == date('Y', time()) && $element['#value']['month'] < date('m', time())) {
form_error($element, t('Please enter a valid expiration date.'));
}
}When the form is submitted, this validate function is called because we defined that in #element_validate in our element definition. Its a rather simple check to see that the person has submitted a valid expiration date, which is nothing more than the date not being in the past if the year is the current year. So, a user should not be able to submit January 2012, for example.
Now what?
Great! Now you have some new field types for Drupal Form API to use. Let's use them! It is really easy to do:
$form['credit_card_number'] = array(
'#type' => 'creditfield_cardnumber',
'#title' => 'Credit Card Number',
'#maxlength' => 16,
);
$form['expiration_date'] = array(
'#type' => 'creditfield_date',
'#title' => 'Expiration Date',
);
$form['credit_card_cvv'] = array(
'#type' => 'creditfield_cvv',
'#title' => 'CVV Code',
'#maxlength' => 4,
'#description' => 'Your 3 or 4 digit security code on the back of your card.',
);Just your straightforward Form API code! That's it! You don't have to write any form validation callbacks, because the elements will fire them automatically, but you can still add more if you want. Now that there are new elements, you can create multiple forms without duplicating the validation code in each one.
If you'd like to see the rest of the code, including credit card number validation, check out the Creditfield module.


