Drupal Theme from Scratch Drupal Theme from Scratch icon
Theming a Drupal 7 Website



Advanced Drupal 7 Theming Techniques

Image or Video?

node--event.tpl.php

Let's say in your Event content type that you want to display an image on some pages and a video in its place on others. Drupal will not display empty content fields. So normally you can just leave some fields blank and then the content won't be displayed. But we still need to tell the template what to do for each case.

<div class="eventMedia">
<?php 
  if(!empty($content['field_video'])){
    print render($content['field_video']);
  }
  else{
    print render($content['field_main_image']);
  }
?>
</div>

Create a new field in the Event content type called video. Then in your node--event.tpl.php file, replace your original 'field_main_image' snippet with the above snippet.

This code uses an IF/ELSE PHP function. It checks to see if the current Event's 'video' field is not empty (!empty) and if it isn't, then render the 'video' field's content. Otherwise, render the 'main image' field's content. In this scenario, the only way the DIV with class eventMedia would be empty in the browser is if no image or video were added to the node.

An alternative solution in case your users can't make up their minds would be this...

<?php 
  if(!empty($content['field_video'])){
    print '<div class="eventMedia">';
    print render($content['field_video']);
    print '</div>';
  }
  elseif(!empty($content['field_main_image'])){
    print '<div class="eventMedia">';
    print render($content['field_main_image']);
    print '</div>';
  }
  else{
    print '';
  }
?>

If both are empty, then neither should display anything.


Less Templates Thanks to IF

node--event.tpl.php

The node--event.tpl.php template will allow us to use one template for all the Event pages because they are of the same content type. But what if one of your event pages requires some special content added to it?

Drupal allows you to do this by creating a more specific node ID template like node--33.tpl.php if the node ID for that Event page is 33. But going down that path means you need to remember which node ID is for what page. I like to keep the number of templates I create as low as possible. Using another IF/ELSE function will allow us to slip in multiple scenarios into one template.

<?php
  // add specific content based off of the page title...
  // get page title (must be written exactly as in Drupal 'title' field)...
  if($node->title == 'Regional Event'){
?>
  <div class="row" style="background-color:blue;">
    <div class="container">
      <p style="color:white;">This is a Regional Event.</p>
      <?php print views_embed_view('regional_event_view', 'block'); ?>
    </div>
  </div>
<?php
  }
?>

The code snippet above checks to see if the current page (node) title is 'Regional Event'. If it is, the page will render the specified content. If it is not, then it will ignore the new content, including the HTML markup.

You can do the same thing using the node ID (nid) rather than the node title. Using NIDs instead is good because a page's nid will never change, but the title could. If that happens, the associated pages will break. Using node titles makes it easier to read and remember what the code is doing. It comes down to personal preference and what makes more sense to you.


Taxonomy Based Content

node--event.tpl.php

Another IF/ELSE scenario could be when you have one template being used by 20 pages but half of them will need extra content (the same extra content). In that case, you might create a taxonomy for all 20 pages and call it something like 'event type'. 10 of them might use a term called 'regional' and the other 10 might use a term like 'local'. The snippet below is how this would work...

<?php
  // create an array for the terms...
  $taxonomyTerms = array();
  // check the node's taxonomy terms to adjust content accordingly...
  $tags = field_view_field('node', $node, 'field_event_type');
  // 'field_event_type' is the machine name of the field in the content type that contains the taxonomy in this example
  foreach($tags["#items"] as $tag){
    $taxonomyTerms[] = $tag["taxonomy_term"]->name;
  }
  // if node uses specified term, add content...
  if( in_array('regional',$taxonomyTerms) ){
    print "This is a REGIONAL event.";
  }
?>

The above code starts by creating a blank array and then looks up the terms that were inputed into the node's event type field using the Drupal function field_view_field. Next it proceeds to add each name of the terms to the array. Finally it checks to see if 'regional' was one of the terms added to the array and if so, then display the proper content.


Break Up the Date Fields

node--event.tpl.php

If you're like me and you use the Date module to create an 'event' with both a start and end date and you want to format the dates in your own special way, you'll be pulling your hair out for a week until someone hands you the answer. So to save your scalp, here's what a friend of mine helped me come up with (thanks, Joe).

The first line of PHP in the code below grabs the data from the node's 'date' field. The field_get_items function in Drupal, if I understand it correctly, grabs the field data which is in an array. Here it is assigned to my $eventDate variable. I pull out the first 'value' from the array and the PHP function, date_create, turns it into a Datetime Object. Next I'm using PHP's date_format twice and creating two variables ($day1 and $month1). $day1 uses the 'jS' format code which will display the date like so: 1st. $month1 uses the 'F' format code which will display the month like so: January.

I do the same thing for 'value2' to format the end date.

Next step is to render the data. I start with the first month and day variables separated by a space to look like this: January 1st. Then we follow that up with some IF / ELSEIF / ELSE function to check if the second month is the same as the first month.

Lastly, we format the time of the event.

<div class="eventDate">
<?php 
  $eventDate = field_get_items('node', $node, 'field_date');

  // get first date data...
  $date1 = date_create($eventDate[0]['value']);
  $day1 = date_format($date1, 'jS');
  $month1 = date_format($date1, 'F');

  // get second date data...
  $date2 = date_create($eventDate[0]['value2']);
  $day2 = date_format($date2, 'jS');
  $month2 = date_format($date2, 'F');

  print $month1.' '.$day1;
  // if start date and end date are not in the same month...
  if($month1 != $month2){
    print " - ".$month2.' '.$day2;
  }
  // else if dates are in the same month...
  elseif($month1 == $month2 && $day1 != $day2){
    print " - ".$day2;
  }
  // else one day event...
  else{
  }
?>
</div>
<div class="eventTime">
<?php
  $time1 = date_format($date1, 'g:i A');
  print $time1;
?>
</div>

The final results I was shooting for was to have the date and time displayed like one of the following three possibilities:

January 30th - February 2nd
1:00 PM

January 1st - 3rd
1:00 PM

January 1st
1:00 PM

Please understand that the formatting of the dates and time using the format codes (like 'jS' and 'F') can be achieved inside Drupal by creating Date Formats. However, the whole if / elseif / else function would not be possible without using our own PHP within the template.


Passing Variables to Views

node--event.tpl.php

When you need a View to look the same everywhere it's displayed (meaning it uses the same HTML/CSS) but you want the content to change in each instance, then you need to pass a variable to the View.

For instance, maybe you've created a View to show video data that includes a title, a video URL, a photo and some descriptive text. You want this info laid out the same way every time. You want it to show up on every Event page. But you only want the video related to the event to show up and you DON'T want to enter the data into Drupal every time.

We will use the field_get_items function again and Contextual Filters to create a dynamic View.

  1. Create a Video content type with fields like title, video URL, image and description. The title field will control what shows up in the View when it's embedded.
  2. Create some Video content.
  3. Create a new Entity Reference field called "Video" in your Event content type. This may require installing the Entity Reference module.
  4. Create a View based on the Video content type.
    • Create your View as you normally would based off your Video content type and fields (we'll call this View "Video - Events").
    • Click the Advanced link on the right of the Views screen to reveal more options.
    • Click the Add button near Contextual Filters.
    • Check off the field that will be used to filter the View (we'll use the "title" field).
    • Click Apply.
    • Under "When the filter value is NOT available" select "Provide default value" and then select "Content ID from URL" from the drop down.
    • Click Apply.
    • Test your View down in the Preview section at the bottom of the page by typing in a video title that coincides with video content you've added (in other words: if you type in "Annual Picnic" and you have no video titled "Annual Picnic", nothing will show up).
    • Save your View once you have decided that it works correctly.
  5. Create a template for your view.
  6. Embed your View block inside the node--event.tpl.php template using similar code as below...
  7. In our Event page, we will select a video by its title from our new drop down field called "Video" and then re-publish the page.
<?php
  // if video exists for node, then proceed to render DIV...
  // get value from field_video to pass to View (if video exists)...
  $video = render($content['field_video']);
  if(!empty($video)){ // if the page has a video...
    // adds 'video' Views block...
    print views_embed_view('video_events', 'block', $video);
  }
?>

The first lines of code check to make sure that a video exists for the current Event page. If it does, then we take the "video" data and add it as the third parameter in the View command.

Now you have a content type to keep all your video data separate and a dynamic View to pull in that data where you need it.


Use IF/Else to Alter HTML Tags

node--event.tpl.php

I set up my templates, like I said before, so that one template could control all the pages for one content type. But sometimes I need the HTML in the template to be a little different for certain pages. One example here illustrates how we swap an H1 tag for a P tag. If the current page is the main About page, then we want the title to use H1, otherwise all other pages should use a P tag because our actual page title will appear under this with an H1 tag and we only want one H1 on the page (for symantics).

<?php if($title == 'About'){ ?>   
  <h1 class="main-title">About Drupal</h1>
<?php } else { ?>
  <p class="section-title">About Drupal</p>
<?php } ?>

If you look closely, you'll see we swap CSS classes as well.

You can do the same thing using the node ID (nid) rather than the node title. Using NIDs instead is good because a page's nid will never change, but the title could. If that happens, the associated pages will break. Using node titles makes it easier to read and remember what the code is doing. It comes down to personal preference and what makes more sense to you.


Add "Load More" (infinite scroll) to Search Results

search-results.tpl.php

We chose to use the "Load More" Ajax feature in our Views pages. Unfortunately, Drupal uses pagination for its search results. In order to streamline the "Load More" effect across the website, we found a Javascript solution online that was easy to add into our custom search templates.

Copy the search-result.tpl.php and the search-results.tpl.php templates from the modules > search folder and paste them into your template folder.

  • search-result.tpl.php: is for customizing each search result
  • search-results.tpl.php: is for customizing the search results page

In your search-results.tpl.php template wrap the <?php print $search_results; ?> line inside a DIV with your own class name and add all the code that comes after <?php print $pager; ?>.

<div class="search-results">
  <?php print $search_results; ?>
</div>

<?php print $pager; ?>

<?php if($totalResults > 10){ // don't show Load More if less than 11 results ?>
  <div>
    <ul class="pager-load-more">
      <li><button class="load-more-ajax">Load more</button></li>
    </ul>
  </div> 
<?php } ?>

<script type="text/javascript">
  (function($){
    Drupal.behaviors.loadMoreAjax = {
      attach: function (context, settings){
        $('.load-more-ajax', context).click(function(){
          var nextPage = $('.pager .pager-next a').attr('href');
          var lastPage = $('.pager .pager-last a').attr('href');
          $.get(nextPage, function(data){
            $(data).find('.search-results').insertBefore($('.item-list'));
            $('.item-list .pager').remove();
            if(nextPage == lastPage){
              $('.load-more-ajax').remove();
            }
            else{
              $(data).find('.item-list .pager').appendTo($('.item-list'));
              Drupal.attachBehaviors($('.item-list'));
            }
          });
        });
        $('.item-list .pager').hide();
      }
    };
  })(jQuery);
</script>

The portion in yellow may not be necessary for everyone. But in my case, the "Load More" button would appear even if there were no other results to show. To prevent this I had to add the IF/ELSE statement in yellow. Good 'ole IF/ELSE.

You can get a further explanation here: Transform any Drupal pager into an autopager - infinite scroll pager - load more pager. Above is the version I used. You will just need to replace what's in blue with your own class name.


Add "Load More" (infinite scroll) to Taxonomy Pages

There is a way to create a View that will replace the default taxonomy page and therefore allow you to utilize the "Load More" effect from within the View. However, to prevent confusion we decided to use the same script as we did with the search results (as noted above). This not only makes things a little less confusing but it also saves us from creating a new View as well as another View template.

The only differences between using the above script in the search-results.tpl.php template and the script below in the page.tpl.php template is that you may want to use a different class name since you'll be wrapping a DIV around the <?php print render($page['content']); ?> line and not a search-results line.

Also, the <?php print $pager; ?> line doesn't exist within the page.tpl.php template, so you will have to add it in yourself.

<div class="taxonomy-results">
  <?php print render($page['content']); ?>
</div>

<?php print $pager; ?>

<?php //if($totalResults > 10){ // don't show Load More if less than 11 results ?>
  <div>
    <ul class="pager-load-more">
      <li><button class="load-more-ajax">Load more</button></li>
    </ul>
  </div> 
<?php //} ?>

<script type="text/javascript">
  (function($){
    Drupal.behaviors.loadMoreAjax = {
      attach: function (context, settings){
        $('.load-more-ajax', context).click(function(){
          var nextPage = $('.pager .pager-next a').attr('href');
          var lastPage = $('.pager .pager-last a').attr('href');
          $.get(nextPage, function(data){
            $(data).find('.taxonomy-results').insertBefore($('.item-list'));
            $('.item-list .pager').remove();
            if(nextPage == lastPage){
              $('.load-more-ajax').remove();
            }
            else{
              $(data).find('.item-list .pager').appendTo($('.item-list'));
              Drupal.attachBehaviors($('.item-list'));
            }
          });
        });
        $('.item-list .pager').hide();
      }
    };
  })(jQuery);
</script>

In order for this method to work you will need to add an IF/ELSE function to your page.tpl.php file and it will need to determine if the current page is a taxonomy page. I did his by analyzing the current URL.


Use an Image for a CSS Background

node--event.tpl.php

<?php 
  $url = $GLOBALS['base_url']; // grabs the site url
  if( !empty($content['field_event_image']) ){      
    $image = field_get_items('node', $node, 'field_event_image');
    $imageURI = $image[0]['uri']; // grab image URI
    $imageFileURL = explode('/', $imageURI); // break up URI to get filename
  }
?>
<div style="background-image: url(<?php print $url.'/sites/default/files/images/event/'.$imageFileURL[4]; ?>);">

When you grab the content of an image field from Drupal, Drupal gives you the HTML tag and attibutes as well as the file path. In this case, we just want the file name otherwise our CSS will explode.

First we grab the website's URL. Next we check to see if an image exists for the current page/node. Then we grab the data from Drupal using field_get_items. Next we pull out the URI from the resulting array. Lastly, we break the URI into a new array so that we can access the actual file name from the URI and then embed it into our CSS code.


Show 4 Possible Content Scenarios Based on Field Data

node--event.tpl.php

This one gave me a bit of a struggle. Just when I thought I had it right, it was still wrong (and additional requests altered what it needed to do). It's all a matter of getting your IF/ELSE statements in the correct order.

The object here was to check the current date ($today), compare it to the event's start date ($eventDayOne), check to see if the "No Registration" field was checked and see if it's a customer only event. These are the resulting scenarios:

  • If the event hasn't happened yet and the "field_no_registration" field is checked (a checkbox in the backend), show nothing.
  • If the event hasn't happened yet and the "field_no_registration" field is not checked and the "field_registration_form_url" field is empty, show "Registration Opening Soon".
  • If the event hasn't happened yet and the "field_no_registration" field is not checked and the "field_registration_form_url" field is not empty, show the "Register Today!" button.
  • If the event has gone by, show nothing.
  • If the event is a customer only event, then add the relative text.
<?php
  // registration button...
  $noReg = field_get_items('node', $node, 'field_no_registration'); 
  $regFormURL = $content['field_registration_form_url'];
  $customerOnly = field_get_items('node', $node, 'field_customer_only_event');
  
  registration_button($eventDate, $noReg, $regFormURL, 0); 

  function registration_button($eventDate, $noReg, $regFormURL, $customerOnly){
    $today = strtotime("now");
    $eventDayOne = strtotime($eventDate[0]['value']);
    // if the event hasn't happened yet...
    if($eventDayOne > $today){
      // if the 'No Registration' field is checked, show nothing...
      if($noReg[0]['value'] == 1){
        print '';
      }
      else{
        // if URL has been provided, show button...
        if( !empty($regFormURL) ){
          print '<div><a class="btn--orange" href="';
          print render($regFormURL);
          print '">Register Today!</a></div>';
          if($customerOnly[0]['value'] == 1){
            print '<p>* This event is for customers only.</p>';
          }
        }
        else{
          print '<p><strong>Registration Opening Soon</strong></p>';
        }
      }
    }
    // event has gone by...
    else{
      print '';
    }  
  }
?>

I turned this into a function because I needed to use it several times in my template.

We start by grabbing the data from the "no registration", "registration form url" and "customer only event" fields and assign them to variables. In my project the first and third items were check boxes while the second item (registration form url) was a regular text field.

Next, I pass the variables to my function along with the Event's date data. The function gets the current date ($today), checks the Event's start date ($eventDayOne) and starts comparing the two. If the Event date is older than the current date, it doesn't display anything. If the Event date hasn't happened yet, then it proceeds to the next bit of code.

Next it checks to see if the "no registration" box was checked ($noReg). If it is checked, then it displays nothing. If it is NOT checked, then we proceed with creating the button.

But before we can create the button, we need to check for the existance of a registration URL ($regFormURL). If that hasn't been entered, then it shows the "opening soon" text. If the URL exists, then it creates the button.

Finally, we check to see if this is a customer event. If it is, then it adds the necessary text.


Identify a Search Result's Content Type

search-result.tpl.php

If you have a lot of content types across your site and you're allowing Drupal to index all of them, it may help your users if you include this trick. The code grabs the node's type and depending on what that type is will determine what site section label is displayed in a smaller font next to the result title/link.

<?php 
// get node ID...
    $nid = $variables['result']['node']->nid;
    $node = node_load($nid);
    $nodeType = $node->type;

    switch($nodeType):
      case 'news_article':
          $contentType = 'News';
      break;
      case 'event':
      case 'event_trade_show':
      case 'event_simple_page':
          $contentType = 'event';
      break;
      default:
          $contentType = '';
      break;
    endswitch;
  ?>
  
  <h3><a href="<?php print $url; ?>"><?php print $title; ?></a> <span style="font-size:.6em; font-weight:normal;"><?php print $contentType; ?></span></h3>

After it grabs the $node->type (the content type), it goes through the list of content types available in the switch. In the case of events, if the content type is either "event", "event trade show" or "event simple page", then the "section" is "event". Therefore any search results that fall under these three content types will be labeled as an "Event" in the results.


Wrap a DIV around an Exposed Form

views-exposed-form.tpl.php

We added a filter to our main event page which is generated by a Views Page. To style the form generated by the Views module, we simply add a DIV with our own class in the views-exposed-form--event-main-page-filter--page.tpl.php

<div class="some-class">
  <div class="views-exposed-form">
    <div class="views-exposed-widgets clearfix">
     
      ... rest of theme's code ...
      
    </div>
  </div>
</div>

You can now use your "some-class" to overide any classes within the form or add styling to elements that have no classes like so...

.some-class .form-checkboxes .form-item {
  display: block;
  float: left;
  margin-right: 2.35765%;
  margin-top: 0.3em;
  width: 31.7616%;
}
    
.some-class legend {
  font-size: 1.2em;
  line-height: 2em;
  width: 100%;
}

The template itself was created by copying the views-exposed-form.tpl.php template from sites > all > modules > views > theme folder and pasting it into our template folder.


Add 'Edit Page' Links to the Top of Pages

node--event.tpl.php

Because we're making our own templates and removing a lot of excess, one of the things I lost which I wanted to get back was those helpful 'Edit' links that appear on your page when you are logged in. To add them back, just use the following code in each of your node level templates.

<?php if(node_access('update',$node)){ print '<span style="font-size:12px;">'; print l( t('Edit Page'),'node/'.$node->nid.'/edit' ); print "</span>"; } ?>

*If you do not see the social network icons here, it is because you have an ad-blocker running. Deactivate it for this site if you want to share this link. It is safe. There are no ads on this site.

This documentation is a work-in-progress. As I continue to learn more about the software, I will try to keep this documentation updated. I'm no expert of anything, so if anyone feels they need to correct me about something I've written or wish to add some additional tips, feel free to mention it in the comments.

"Drupal 7 Theme from Scratch" was written by TenTen71 because no one else would.