How to change the HTML markup in a field with a theme function in Drupal

Ever wanted to change the Drupal 7 markup? Get rid of all those pesky divs? If so, this is article for you. I will give you a simple example how to do this not by changing the site theme (template.php) but by using a theme function.

Yes, you have make a module, and yes, I will show you have to make it. You will also find, attached to this article, the complete code wrapped up in a zip-file. Using theme functions is not that hard so relative new Drupal users and themers should be able to follow, though, a basic PHP knowledge might come in handy.

What we are trying to accomplish

This example have a content type containing a single text field. The HTML markup from this field will we change using a theme function.

The text field is for the examples sake a multivalued field. This allow me to give a example on how to loop over more then one field entry.

Simple content type

It might be a good idea to create a new content type, containing only a single field like in the example. This and follow this example line by line and when your code work work, change it to match your content types and fields.

Why we are doing this

Have a look at the markup bellow generated by Drupal core. It is from a single field with two entries. View the node and inspect the markup by your self.

<div class="field field-name-field-story field-type-text field-label-inline clearfix">
  <div class="field-label">
    Story:&nbsp;
  </div>
  <div class="field-items">
    <div class="field-item even">
      my content goes into this field
    </div>
    <div class="field-item odd">
      My second part of content
    </div>
  </div>
</div>

HTML grabbed from Drupal 7 running the Stark theme.

More divs and classes then you ever gonna need. Let us try to clean this up. I kept the field class as an example but you may omit this class also.

<p class="field-story">
  my content goes into this field
</p>
<p class="field-story">
  My second part of content
</p>

Background information that might come in handy

Optional section. For those who know it all, skip to next chapter. As I mention earlier there is nothing stopping you from overriding this in your theme template.php. That if the field or what ever thing you are trying to override have a template function. One way of thinking of theme functions is something Drupal run whenever it try to view (render) content. These functions is kept in a registry that keep a track of all functions and who and where they belong. This registry also allow us to change the default function that run for a field (and more) and that is exactly what we are going to do.

Finding what function to override can take some practice but a great starting point is by trying to use the theme developer module. Think of it like the Firebug of Drupal. It allow you to click on a piece of content and it will tell you what it is, what function that created it and where it came from.

theme_field devel themer

Pointing on my field in a node tell me theme_field. Reading the API for the first, or second time, can be a put off. Don't worry it's not as hard as it look like. We know what function that does the thing and the page contain the code we going to change.

Create an empty module

The very first thing we need to do is to create a empty module that does nothing. Nothing is better then blowing up, right? If this is the first time you create a module in Drupal, please do not stop reading. This will not heart and I promise I'll keep this nice and simple. We need two files - module-name.info and module_name.module. I'm going to call this module 'smb'. This means that files and functions (hooks) will be named smb_something. Name your module what ever you like, but it is a good idea to come up with some unique and make sure you replace all text where I use smb with your module name.

  • Locate sites/all/modules, create a directory called smb and open the director.
  • Create a file called smb.info that contain:
    name = theme override example.
    core = 7.x
  • Create a file called smb.module that we for know keep almost empty.
    <?php
       

That is all Drupal really need. The module now show up in your module list. Enable it as any other module and if you did it right, nothing exciting will happen. Aboslutly nothing will should is happening since the module file did not contain any PHP code to run. This is soon to change.

Register your theme function

In the capter background information I talked about the theme registry. Now it is time to crack it open and try to change it. To alter the registry do we need to register an override. This will alter a already existing registry entry. Drupal is kind enought to offer us a way to do this also hook_theme_registry_alter. Enter the following code (omit <?php) info smb.module.

<?php
function smb_theme_registry_alter(&$theme_registry) {
 
$theme_registry['field']['theme path'] = drupal_get_path('module', 'smb');
 
$theme_registry['field']['function'] = 'smb_theme_field';
}
?>

Two lines, that is all it takes. First line tell Drupal the override is done by a module called 'smb'. Line two tell Drupal what function in smb.module that will take care of the override. Now you see why it is useful to know what theme function you are trying to override. As showned earlier we managed to trick Drupal into telling us where the field was coming from – theme_field so I simply named the function smb_theme_field().

Take control of theme_field

Time to create the function creating the markup we registered in hook_theme_registry_alter(). Got api.drupal.org and locate the function theme_field and a look at the code it is made up of. If you look very closly perhaps you will you will recognise some of the divs wapping the text field. Grab the code and insert it into your smb.module like this.

<?php
function smb_theme_field($variables) {
 
$output = '';

 

// Render the label, if it's not hidden.
 
if (!$variables['label_hidden']) {
   
$output .= '<div class="field-label"' . $variables['title_attributes'] . '>' . $variables['label'] . ':&nbsp;</div>';
  }

 

// Render the items.
 
$output .= '<div class="field-items"' . $variables['content_attributes'] . '>';
  foreach (
$variables['items'] as $delta => $item) {
   
$classes = 'field-item ' . ($delta % 2 ? 'odd' : 'even');
   
$output .= '<div class="' . $classes . '"' . $variables['item_attributes'][$delta] . '>' . drupal_render($item) . '</div>';
  }
 
$output .= '</div>';

 

// Render the top-level DIV.
 
$output = '<div class="' . $variables['classes'] . '"' . $variables['attributes'] . '>' . $output . '</div>';

  return

$output;
}
?>

Notise I changed the function name to match up what we told drupal in hook_theme_registry_alter() it was named. Not that over theme_field also mean this code will run for every field in your installation using it that is why you need to copy the existing working code from the drupal api.

To register this change (make it active ) you have to rebuilt the theme registry. Visit admin/config/development/performance or if you have drush installed (your should), drush cc all.

To verify that it worked. Try using theme developer and expect the field. It should tell you that smb_theme_field() is now running.

theme_field devel themer overridden

Modified theme function

We are now in control. You can change the code and that way change the markup for all fields. In this example will we change the markup for one single field field_story. Change smb_theme_field() to:

<?php
function smb_theme_field($variables) {
 
$output = '';

  if (

$variables['element']['#field_name'] == 'field_story') {
    foreach (
$variables['items'] as $delta => $item) {
     
$output .= '<p class="' . $variables['field_name_css'] . '">';
     
$output .= drupal_render($item);
     
$output .= '</p>';
    }
  }
  else {
   
// Render the label, if it's not hidden.
   
if (!$variables['label_hidden']) {
     
$output .= '<div class="field-label"' . $variables['title_attributes'] . '>' . $variables['label'] . ':&nbsp;</div>';
    }

   

// Render the items.
   
$output .= '<div class="field-items"' . $variables['content_attributes'] . '>';
    foreach (
$variables['items'] as $delta => $item) {
     
$classes = 'field-item ' . ($delta % 2 ? 'odd' : 'even');
     
$output .= '<div class="' . $classes . '"' . $variables['item_attributes'][$delta] . '>' . drupal_render($item) . '</div>';
    }
   
$output .= '</div>';

   

// Render the top-level DIV.
   
$output = '<div class="' . $variables['classes'] . '"' . $variables['attributes'] . '>' . $output . '</div>';

  }

  return

$output;
}
?>

Not much changed but we added an if statement we we make sure the code only run if the field it field_story.

<?php
if ($variables['element']['#field_name'] == 'field_story') {
  foreach (
$variables['items'] as $delta => $item) {
   
$output .= '<p class="' . $variables['field_name_css'] . '">';
   
$output .= drupal_render($item);
   
$output .= '</p>';
  }
}
?>

Useful links

Even more images: 
Simple content type
theme_field devel themer
theme_field devel themer overridden
Attachment: 

Comments

Thanks man.. It solved my problem.. :)

This is fantastic, thank you. I've weighed the possibilities of using Fences, Display Suite, custom field templates, and Semantic Fields but in the end, this is really a nice solution for my use case. From what I understand it's also faster to run code rather than using a bunch of field tpl templates.

Brilliant stuff, exactly what I needed. And I learned a lot. Thank you.

Thank you soo much, this tutorial is AMAZING!!!! May I ask a very stupid question, can this code be placed directly in to the template.php file, if so how? Alternatively, can the same be achieved by creating custom field.tpl.php files? Or does what I'm asking involve a totally different method?

I have not tested running this kind of code in template.php though I see no reason why it should not work. If it is best practice to put this kind of code inside your template.php is another thing.

Add new comment