0

I am writing a custom field module for phone numbers. The field has 4 different parts:

  • A select element to choose the type of phone number, (phone, fax, toll free, other),
  • A text field for the phone number.
  • A text field for the extension.
  • A checkbox to mark the number as the primary number.

I’ve been trying to solve this problem for over a month now. I’ve spent days looking and searching through code of other modules for more than 15 hours at a stretch, several times. I have gone through every tutorial and article I’ve been able to find (there are a lot of them!), and they all leave me lost at the same point.

I have been able to create the field using implementations of:

  • hook_schema()
  • hook_field_info()
  • hook_field_widget_info()
  • hook_field_widget_form()
  • hook_field_is_empty()
  • hook_field_validate()
  • hook_field_formatter_info()
  • hook_field_formatter_view()

Functionally, the field works great. The fields are all there and it saves the data in the database just how I would like. The problem that I run into with this method is that my field looks like this:

vertical field

All of the elements are stacked on each other vertically, and I need them to be horizontal. After struggling for several days figuring out how to theme the widget, and failing, I finally reached out and asked about it here, How to theme form elements in a custom field widget?. The kind gentleman that helped me along suggested that I need to create a custom Form API element to be able to theme it the way I like.

So I went back to the research and found more tutorials and information on how to create custom form elements. Awesome, I was able to make the element, add a css class just where I wanted it, and my field looks as it should:

horizontal field

These are the hooks and functions that did it:

  • hook_element_info()
  • hook_theme()
  • A process function callback
  • A value callback function

Now I’m running into a different problem, and I’m not sure where to begin. The field is not saving the data into the database. It will save a record, but it won’t save the data entered into the fields. The columns for the label, phone number, extension, and primary are just empty. I’m not actually sure how to map the array of data that comes from the element to the fields in the database as set up in the schema.

All the tutorials and articles I’ve found about custom elements don’t explain how to get the data into the database. They all leave off with, “And that’s it, you’re done!”, once the element is created. So that tells me that there must me something easy that I'm missing here, and I would so very much appreciate any help with getting this problem solved. There are gaps in my knowledge. I’m really just learning all this stuff. It’s embarrassing how many hours I have spent tackling this one problem; at least a couple hundred… My project is pretty stuck without this knowledge.

Here’s all the code for my module:

This is in the .install file:

/**
 * Implements hook_field_schema().
 */
function marigold_phone_field_schema($field) {
  return array(
    'columns' => array(
      'label' => array(
        'description' => "The label that defines the phone number type.",
        'type' => 'varchar',
        'length' => 64,
        'not null' => FALSE,
      ),
      'number' => array(
        'description' => 'The phone number.',
        'type' => 'varchar',
        'length' => 24,
        'not null' => FALSE,
      ),
      'ext' => array(
        'description' => 'An extention number.',
        'type' => 'varchar',
        'length' => 24,
        'not null' => FALSE,
      ),
      'primary' => array(
        'description' => 'A boolean value indicating if the instance of the number is the primary contact.',
        'type' => 'int',
        'size' => 'tiny',
        'default' => 0,
        'not null' => FALSE,
      ),
    ),
    'indexes' => array(
      'label' => array('label'),
      'number' => array('number'),
    ),
  );
}

This is in the .module file:

/**
 * Implements hook_element_info().
 */
function marigold_phone_element_info() {
  $elements['marigold_phone'] = array(
    '#input' => TRUE,
    '#process' => array('marigold_phone_element_process'),
    '#theme' => array('marigold_phone_element'),
    '#theme_wrappers' => array('form_element'),
    '#tree' => TRUE,
    '#value_callback' => 'marigold_phone_element_value_callback',
  );
  return $elements;
}

/**
 * Process callback.
 */
function marigold_phone_element_process($element, $form_state, $complete_form) {
  // This element uses the "Select (or other)" contrib module.
  $element['marigold_phone']['label'] = array(
    '#type' => 'select_or_other',
    '#theme_wrappers' => array(),
    '#title' => 'Label',
    '#title_display' => 'invisible',
    '#options' => drupal_map_assoc(array(
      t('phone'),
      t('fax'),
      t('toll free'),
    )),
    '#other' => t('other'),
    '#other_unknown_defaults' => 'other',
    '#other_delimiter' => FALSE,
    '#other_size' => 10,
  );
  if (isset($element['#default_value']['marigold_phone']['label'])) {
    $element['marigold_phone']['label']['#default_value'] = $element['#default_value']['marigold_phone']['label'];
  }

  $element['marigold_phone']['number'] = array(
    '#type' => 'textfield',
    '#theme_wrappers' => array(),
    '#title' => t('Number'),
    '#title_display' => 'invisible',
    '#size' => 16,
    '#attributes' => array('placeholder' => 'number'),
  );
  if (isset($element['#default_value']['marigold_phone']['number'])) {
    $element['marigold_phone']['number']['#default_value'] = $element['#default_value']['marigold_phone']['number'];
  }

  $element['marigold_phone']['ext'] = array(
    '#type' => 'textfield',
    '#theme_wrappers' => array(),
    '#title' => t('Extension'),
    '#title_display' => 'invisible',
    '#field_prefix' => t('ext'),
    '#size' => 3,
    '#attributes' => array('placeholder' => 'ext.'),
  );
  if (isset($element['#default_value']['marigold_phone']['ext'])) {
    $element['marigold_phone']['ext']['#default_value'] = $element['#default_value']['marigold_phone']['ext'];
  }

  $element['marigold_phone']['primary'] = array(
    '#type' => 'checkbox',
    '#title' => t('Primary'),
    '#title_display' => 'after',
    '#return_value' => 1,
  );
  if (isset($element['#default_value']['marigold_phone']['primary'])) {
    $element['marigold_phone']['primary']['#default_value'] = $element['#default_value']['marigold_phone']['primary'];
  }

  return $element;
}

/**
 * Implements hook_theme().
 */
function marigold_phone_theme() {
  return array(
    'marigold_phone_element' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Theme the phone element.
 */
function theme_marigold_phone_element($variables) {
  drupal_add_css(drupal_get_path('module', 'marigold_phone') . '/marigold_phone.css');

  $element = $variables['element'];
  $output = '';
  $output .= '<div class="marigold_phone_inline">' . drupal_render($element['marigold_phone']['label']) . '</div>';
  $output .= '<div class="marigold_phone_inline">' . drupal_render($element['marigold_phone']['number']) . '</div>';
  $output .= '<div class="marigold_phone_inline">' . drupal_render($element['marigold_phone']['ext']) . '</div>';
  $output .= '<div class="marigold_phone_inline">' . drupal_render($element['marigold_phone']['primary']) . '</div>';

  return $output;
}

/**
 * Value Callback
 */
function marigold_phone_element_value_callback($element, $input = FALSE, &$form_state) {
  if ($input !== FALSE) {
    if ($input['marigold_phone']['label'] && !$input['marigold_phone']['number']) {
      $input['marigold_phone']['label'] = '';
    }
    return $input;
  }
  elseif (!empty($element['#default_value'])) {
    return $element['#default_value'];
  }
}

/**
 * Implements hook_field_info().
 */
function marigold_phone_field_info() {
  return array(
    'marigold_phone' => array(
      'label' => t('Phone and type'),
      'description' => t('This field stores a telephone number along with the type of phone it is, (phone, fax, toll free, or other).'),
      'default_widget' => 'marigold_phone',
      'default_formatter' => 'marigold_phone_formatter',
    ),
  );
}

/**
 * Implements hook_field_widget_info().
 */
function marigold_phone_field_widget_info() {
  return array(
    'marigold_phone' => array(
      'label' => t('Phone and type'),
      'field types' => array('marigold_phone'),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function marigold_phone_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {

  $element += array(
    '#type' => $instance['widget']['type'],
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  );

  return $element;
}

/**
 * Implements hook_field_is_empty().
 */
function marigold_phone_field_is_empty($item, $field) {
  if (empty($item['marigold_phone']['number'])) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_field_validate().
 */
function marigold_phone_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {

}

There are two more hooks, hook_field_formatter_info() and hook_field_formatter_view(), but I'll keep this shorter and not include them unless you'd like to see them too.

Problems that I've seen others have with similar issues revolve around the 'is_empty' hook giving false TRUE responses. I've tested mine with the debugger, and it steps into the appropriate return as expected. I think my problem probably lies in the widget form or one of the callback functions... I dunno.

  • i'm not gonna read all your code, i have a client meeting now. But i've used container-inline as seen in the approved answer here lots of times to make things inline drupal.stackexchange.com/questions/7954/… EDIT: I re-read your post and my answer is useless, but I'll leave it here for others. Although if it worked before you made the additional functions for your field -- I would try to make the css changes using hook_form_alter and the technique I linked above ... – tenken Dec 4 '14 at 15:46
  • Thank you. I will try that to see if I can get it working. It doesn't seem as elegant to alter the form as to create it with the appropriate markup to begin with, but if that's what it takes, it'll keep me going. I'm going to be needing to create a lot of these custom fields for this project, so I want it as clean as possible. I know it can be done with examples like the "Link" module or "Address Field". They are able to have nicely themed fields. I've dug and dug into that code, but ultimately end up pretty lost. Thanks for the suggestion. – mlanning Dec 4 '14 at 16:09
  • i think the appropriate markup is whatever makes the form process. CSS will need to be different across numerous devices and platforms always. That's why I don't think hook_form_alter and adjusting CSS that way (or through media queries) is such a huge hack. – tenken Dec 4 '14 at 16:12
  • Well, I finally got it. It doesn't totally answer this question as it is asked, but it works. Taking your advice, tenken, I went back to the code that works. I ditched the idea of a custom element, and went with just a custom field using the field api. In the hook_field_widget_form, for each of the elements, I was able to add a class attribute. I had tried this before but never got it to work. This is what I added that works... '#attributes' => array('class' => array('marigold_phone_inline')) – mlanning Dec 4 '14 at 17:23
  • I had tried this before but never got it to work. The problem I had was not putting the classes in an array. The class container-inline didn't work for me though, so I set my own. – mlanning Dec 4 '14 at 17:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.