The following exercise consists in theming the contact form in Drupal 6. Once we're done, it will look like we'll have created two fall-back-on 'pages' for contact forms, one page/form to 'request a quote' at Randy.com/contact/quote, and an other page/form for general inquiries at Randy.com/contact/info. The trick here — if there's one — consists in theming the contact form differently based on the requested URI.
This exercise will show you how to:
Context: You're having a bad day. Your ex-boyfriend wrote to you that he loves going down on women, while he never went down on you ever for the entire duration of the relationship, and you're developing yet another Drupal site for Randy.com.
Randy believes he can give something back to the world. Doesn't everyone? On his new site, he wants to provide menu links to two different contact forms. One to 'request a quote', and another to ask Randy questions on whatever-else, like 'Randy, what is it you use to have such great hair?'.
You're in luck because in core there's a module to whip out a contact form — and validate it and send you its submitted content by e-mail. This module produces only one contact form, with a 'select an option' drop-down list of 'categories'. For each of these categories, you, the admin, can specify a different e-mail as the destination for the form's submitted content.
After some theming magic, it will look like you'll have created two different forms. But you won't... ah ah ah... After your magic touch, it will look like you'll have created two Drupal pages... but in truth (shhhh...) you definitely won't. Your site will behave as if, and that's all that matters.
To check if your contact module is enabled, try and access the page admin/build/contact/add, or look for 'Contact form' under 'Site building' in your Admin menu. If you land on the 'Site building' page, you have to enable the module. Head over admin/build/modules.
Now, you'll create two Contact categories, one for requesting a quote and an other for general inquiries. Head over admin/build/contact/add. Or, go to Site building → Contact form, then click on the Add category tab. Create a 'request a quote' category like so:
When you're done, you'll be redirected to admin/build/contact. Place your cursor over the 'edit' link next to your category. This link will point to admin/build/contact/edit/x. That number x is the cid of your category (contact hi-dee). Take mental note of it.
Create a 2nd category for general inquiries, with its own e-mail address, etc. You will end up with two categories. If you had created categories before and deleted them, the 'cid' counter has never been reset (it never is), so you may add categories with, e.g., cid 32 and 33 (or 65 and 66). That's fine.
Take note of the 'cid' for general inquiries.
So, now, you want to theme the contact form. That's your goal. The first question you ask yourself is What theming function must I intercept and override?
To find answers to these types of questions, 11heavens.com recommends using the module Theme Developer.
Download the Devel set of modules from Drupal.org, extract it to sites/all/modules, and enable the Devel module on your admin/build/modules page.
Make sure to move the Development block to some active region on your page.
Once you've landed on the contact page, click on the 'Enable Theme developer' link in your Development block.
Now, enable the Themer info widget. The Themer info toggle checkbox sits on the bottom-left of your viewport. (Thought I should throw the fancy word "viewport" in there.)
You want to click on the form, but you end up selecting one field or another within the form. Argh. Solution: click somewhere to the right of the Send e-mail
button.
There's no specific theming function that's been registered to theme the contact form. How do you know that?
How do you know that theme_form() is used to theme the contact form? The Themer info widget tells you that. Take a look.
See it? Function called: theme_form().
You're not interested in overriding the theme_form() function. You want to theme the contact form. You do not want to theme all forms on Randy.com. You want to theme only that particular form. What will you do ?
First, eat something.
You will register a theming function for the contact form.
Modules and themes can both register theming functions. Modules and themes can both implement the function hook_theme.
To register a theming function for a form, you must know its Form ID.
There are at least two ways to find the Form ID.
<form action="/contact" accept-charset="UTF-8" method="post" id="contact-mail-page">
. Hence, the Form ID, here, is contact_mail_page. Replace each hyphen of your id attribute by an underscore and you've got your form ID.<input name="form_id" id="edit-contact-mail-page" value="contact_mail_page" type="hidden">
You take mental note of the Form ID. (You and I both have amazing storage capacities in our brains.)
Just for fun (and to learn a few things), you take a peek inside the contact module to make sure that it provides no theming function for the 'contact_mail_page' form. You're looking for the module's implementation of hook_theme(). In the modules/contact folder, you search for the string function contact_theme.
And you find nothing.
The module is registering no theming function. Most core modules register their share of theming functions. The contact module is quite 'exceptional' in that regard.
For the sake of learning something about HOOK_theme, you'll look at another module's implementation of the hook. Open the file modules/user/user.module and look at lines 30-78.
<?php
/**
* Implementation of hook_theme().
*/
function user_theme() {
return array(
'user_picture' => array(
'arguments' => array('account' => NULL),
'template' => 'user-picture',
),
'user_profile' => array(
'arguments' => array('account' => NULL),
'template' => 'user-profile',
'file' => 'user.pages.inc',
),
'user_profile_category' => array(
'arguments' => array('element' => NULL),
'template' => 'user-profile-category',
'file' => 'user.pages.inc',
),
'user_profile_item' => array(
'arguments' => array('element' => NULL),
'template' => 'user-profile-item',
'file' => 'user.pages.inc',
),
'user_list' => array(
'arguments' => array('users' => NULL, 'title' => NULL),
),
'user_admin_perm' => array(
'arguments' => array('form' => NULL),
'file' => 'user.admin.inc',
),
'user_admin_new_role' => array(
'arguments' => array('form' => NULL),
'file' => 'user.admin.inc',
),
'user_admin_account' => array(
'arguments' => array('form' => NULL),
'file' => 'user.admin.inc',
),
'user_filter_form' => array(
'arguments' => array('form' => NULL),
'file' => 'user.admin.inc',
),
'user_filters' => array(
'arguments' => array('form' => NULL),
'file' => 'user.admin.inc',
),
'user_signature' => array(
'arguments' => array('signature' => NULL),
),
);
}
?>
You make these fine observations:
In the hook_theme() function, the key to each sub-array is the internal name of the hook. And we mean hook in "themespeak" (theme parlance), not "modulespeak". For a form, the name of the hook is the Form ID. Each sub-array contains info about said hook, and that info is an associative array. Among the info, there's only one required item to provide, and that is the 'arguments'. Plural form.
So, now, you'll register a theming function for the contact_mail_page form. The contact module hasn't done so. So, you will.
You open your theme's template.php file. Randy.com uses the Garland theme. (For the money he's paying you, he better not fuss.) Here's the code to register a theming function for the 'contact_mail_page' form:
<?php
/* Register some theme functions for forms, theme functions
* that have not been registered by the module that created
* these forms...
*/
function garland_theme(){
return array(
'contact_mail_page' => array(
'arguments' => array('form' => NULL),
'template' => 'contact-mail-page',
),
);
}
?>
You take good note of the following:
NULL
.When you want to override a template provided by a module, the process is the same, always. You locate the template file and copy it to your theme folder. When you want to pass additional variables to the template, you define a preprocess function for the template in your theme. That second step, however, is optional. It's usually not needed.
In your case, the module contact has provided no default template. Heck, it has not even provided a theming function! You had to register one! So you must go through these two steps:
By the way, are you still using global $user;
in your template files? Just use $user
, it's a default baseline variable available in all *.tpl.php files. It's part of your free meal.
For each template file used to theme content in Drupal 6, there must exist a function to pass to it whatever variables are needed to output content. For each template file used to theme content in Drupal 6, there exists somewhere a preprocess function.
Let's move on.
Create a file called contact-mail-page.tpl.php in your theme folder. In it, place this markup:
<p>So you would like to contact us...</p>
<div class="submit-contact-form"><?php print $form_markup; ?></div>
Then, create the skeleton for the template preprocess function in your template.php file like so:
<?php
function garland_preprocess_contact_mail_page(&$vars) {
$vars['form_markup'] = drupal_render($vars['form']);
}
?>
The function garland_preprocess_contact_mail_page(), as defined above, creates a new variable and passes it along to the template file. That new variable is $form_markup, and it contains the markup for the form 'contact_mail_page'.
You know this already about $vars:
You know this already about drupal_render():
Clear your Drupal cache. That will clear the Theme registry.
After you've refreshed your page in your browser, you should see a 'So you would like to contact us...' greeting between the title of the page and the contact form itself. The top of the contact page should look like this:
Satisfied and relieved, you remove the 'So you would...' paragraph from the template file.
So you want to produce two 'fake' Drupal pages, each one displaying a different version of the contact form, a version tailored to suit the context of a quote request, and another one to suit the context of a general inquiry. You will create 2 files, one for each 'variation'. You'll name the first template file 'contact-mail-page-quote.tpl.php' and the second one 'contact-mail-page-info.tpl.php'. Make two copies of your contact-mail-page.tpl.php template, you'll work from it. Modify the names of these copies, and change their content.
You now have three 'extra' templates in your theme:
Edit the content of contact-mail-page-quote.tpl.php to:
<p>You would like to request a quote...</p>
<div class="contact-for-quote"><?php print $form_markup; ?></div>
Edit the content of contact-mail-page-info.tpl.php to:
<p>You have an inquiry...</p>
<div class="contact-for-general-inquiry"><?php print $form_markup; ?></div>
Only contact-mail-page.tpl.php is 'seen', so far, by the theming engine. You'll change that.
In the URI http://Randy.com/contact/quote, 'contact/quote' is $_GET['q']. Although it is a path that leads to nowhere, it is still something you can read. To read what's contained in the q value, you will use the Drupal function arg().This function breaks down the Drupal path into its components. Components of the path are separated by a forward-slash. For the URI http://Randy.com/contact/quote, arg(0) is the 1st component, ie: 'contact', and arg(1) is the 2nd component, ie: 'quote'. You will read the second component, and will suggest a template file based on it.
<?php
function garland_preprocess_contact_mail_page(&$vars) {
// If the visitor types contact/quote or contact/info
switch(arg(1)) {
case 'quote' :
$vars['template_file'] = 'contact-mail-page-quote';
break;
case 'info' :
$vars['template_file'] = 'contact-mail-page-info';
}
$vars['form_markup'] = drupal_render($vars['form']);
}
?>
You are suggesting one template file for each case by using $vars['template_file']. You assign to your 'suggestion' variable the name of the *.tpl.php file you plan on using minus the extension. Just like you did in your implementation of HOOK_theme when you told the theming engine to use 'contact-mail-page', which effectively points to the file contact-mail-page.tpl.php.
Now, you're thinking about how you want to render or display the contact form for each situation, the 'quote' situation and the 'info' one. In both situations, you do NOT want to show the drop down selection for categories. A category is picked based on the requested URI. The URI already informs us as to what category was chosen, in other words. So you'll hide that element in the form in both cases. For that you'll use CSS display:none. However, when the form is submitted, that information about the cid will be missing, so you must 'fill in' that form field behind the scenes, on behalf of the visitor. This is something that you can do in your theme.
The values you'll assign here to $vars['form']['cid']['#value'] are the Contact Hi-dee you had 'memorized' previously. So they may not be 1 and 2.
<?php
function garland_preprocess_contact_mail_page(&$vars) {
/* If the visitor types contact/quote or contact/info,
otherwise the default template is used, ie: contact-mail-page */
switch(arg(1)) {
case 'quote' :
$vars['form']['cid']['#value'] = 1;
$vars['form']['cid']['#prefix'] = '<div style="display:none;">';
$vars['form']['cid']['#suffix'] = '</div>';
$vars['template_file'] = 'contact-mail-page-quote';
break;
case 'info' :
$vars['form']['cid']['#value'] = 2;
$vars['form']['cid']['#prefix'] = '<div style="display:none;">';
$vars['form']['cid']['#suffix'] = '</div>';
$vars['template_file'] = 'contact-mail-page-info';
}
$vars['form_markup'] = drupal_render($vars['form']);
}
?>
Clear your Drupal cache, to clear the Theme registry.
Then, head over to the 'contact/quote' page. What do you see? Test the 'contact/info' page as well.
To troubleshoot any problems you may encounter while editing your form, you need to inspect it. The devel module provides a kit of 5 functions to inspect the content of variables:
Function | To inspect content of | Content shown | Using a recursive function |
---|---|---|---|
dsm() or dpm() | Any variable | In a message | Yes, perfect for all. |
dvm() | A complex variable, such as an object or array | In a message | No. Ugly print. |
dpr() | A complex variable, such as an object or array | At the top of the page. | Yes. Perfect for complex, ie: nested, objects and arrays. |
dvr() | Any variable | At the top of a page. | No. Ugly print. |
To inspect complex nested arrays such as forms, you can use either dpr() or dpm()/dsm(). These functions pretty-print forms either at the top of the page, or in the 'message' area. dpm() and dsm() have the same behavior.
In the 6.x-1.8 version of the Devel modules, dsm and dpm print the content of variables at the top of the page. That's a bug. The m in dsm and dpm means 'message' — it uses the function drupal_set_message().
To make sure you set the value for cid properly in your theme preprocess function, you can check the content of the $vars['form']['cid'] field within your code, like so:
<?php
$vars['form']['cid']['#value'] = 2;
...
$vars['form']['cid']['#value'] = 1;
...
// Checking the content of the cid field
dsm($vars['form']['cid']);
...
?>
You now want to modify the field labels in the contact form.
<?php
function garland_preprocess_contact_mail_page(&$vars) {
/* If the visitor types contact/quote or contact/info,
otherwise the default template is used, ie: contact-mail-page */
switch(arg(1)) {
case 'quote' :
$vars['form']['contact_information']['#value'] = t('We will get back to you with a quote within 48 hours.');
$vars['form']['message']['#title'] = t('What you need');
$vars['form']['subject']['#title'] = t('Name of your company');
$vars['form']['cid']['#value'] = 1;
$vars['form']['cid']['#prefix'] = '<div style="display:none;">';
$vars['form']['cid']['#suffix'] = '</div>';
$vars['template_file'] = 'contact-mail-page-quote';
break;
case 'info' :
$vars['form']['contact_information']['#value'] = t('Send us your question. A real person will get back to you very shortly.');
$vars['form']['message']['#title'] = t('Your question');
$vars['form']['cid']['#value'] = 2;
$vars['form']['cid']['#prefix'] = '<div style="display:none;">';
$vars['form']['cid']['#suffix'] = '</div>';
$vars['template_file'] = 'contact-mail-page-info';
}
// dsm($vars['form']);
$vars['form_markup'] = drupal_render($vars['form']);
}
?>
Now head over to the 'contact/quote' page. (Also look at the other version of the contact form, at 'contact/info'.)
You ask yourself: Why in the hell did I create template files when all they do, essentially, is print a variable? They barely contain any HTML markup! All contact-mail-page-info.tpl.php contains, for example, is this:
<p>You have an inquiry...</p>
<div class="contact-for-general-inquiry"><?php print $form_markup; ?></div>
The paragraph that contains 'You have an inquiry...' can certainly be added to the form as a markup field. At the theme layer, one can still add additional fields to a form, even though they will not be validated, neither submitted. They will be added just for the 'show'.
You can also wrap the form within a div with a specific class name, using $vars['form']['#prefix'] and $vars['form']['#suffix']. That way, you can take care of supplying the form with a CSS class name of contact-for-general-inquiry
.
So, you've changed your mind about all this. You delete the three template files. All three. (You heard that using a template is five times slower than using a theming function.)
Then, you open your template.php file. You remove from the garland_theme() function any mention of the template file 'contact-mail-page.tpl.php'. The function becomes:
<?php
function garland_theme(){
return array(
'contact_mail_page' => array(
'arguments' => array('form' => NULL),
// 'template' => 'contact-mail-page',
),
);
}
?>
Then, you go ahead and comment out the whole preprocess function. Preprocess functions are for template files only.
I repeat...
Next, you write your theming function. This consists in moving the code from the commented-out preprocess function to a new function you call garland_contact_mail_page($form). In that new function, you replace all mention of $vars['form'] with $form. A 'find and replace' tool will be quite useful here. What the theming function must do is return themed content. Hence, you delete the line of code which consisted in adding a variable to $vars, and place the following 'return' instruction in its place:
<?php
// returning the themed form
return drupal_render($form);
?>
Here's the code for the entire function:
<?php
function garland_contact_mail_page($form) {
/* If the visitor types contact/quote or contact/info,
otherwise the default template is used, ie: contact-mail-page */
switch(arg(1)) {
case 'quote' :
$form['intro']['#value'] = '<p>' . t('You would like to request a quote...') . '</p>';
$form['intro']['#weight'] = -40;
$form['contact_information']['#value'] = t('We will get back to you with a quote within 48 hours.');
$form['message']['#title'] = t('What you need');
$form['subject']['#title'] = t('Name of your company');
$form['cid']['#value'] = 1;
$form['cid']['#prefix'] = '<div style="display:none;">';
$form['cid']['#suffix'] = '</div>';
$form['#prefix'] = '<div class="contact-for-quote">';
$form['#suffix'] = '</div>';
break;
case 'info' :
$form['intro']['#value'] = '<p>' . t('You have an inquiry...') . '</p>';
$form['intro']['#weight'] = -40;
$form['contact_information']['#value'] = t('Send us your question. A real person will get back to you very shortly.');
$form['message']['#title'] = t('Your question');
$form['cid']['#value'] = 2;
$form['cid']['#prefix'] = '<div style="display:none;">';
$form['cid']['#suffix'] = '</div>';
$form['#prefix'] = '<div class="contact-for-general-inquiry">';
$form['#suffix'] = '</div>';
}
// dsm($form);
return drupal_render($form);
}
?>
Even with a '#weight' of -40 applied to the added 'intro' markup field, it still sinks to the bottom of the form. You can use drupal_render() to make sure that the intro text always appears on top, like so:
<?php
function garland_contact_mail_page($form) {
/* If the visitor types contact/quote or contact/info,
otherwise the default template is used, ie: contact-mail-page */
switch(arg(1)) {
case 'quote' :
$form['intro']['#value'] = '<p>' . t('You would like to request a quote...') . '</p>';
$form['contact_information']['#value'] = t('We will get back to you with a quote within 48 hours.');
$form['message']['#title'] = t('What you need');
$form['subject']['#title'] = t('Name of your company');
$form['cid']['#value'] = 1;
$form['cid']['#prefix'] = '<div style="display:none;">';
$form['cid']['#suffix'] = '</div>';
$form['#prefix'] = '<div class="contact-for-quote">';
$form['#suffix'] = '</div>';
$output = drupal_render($form['intro']);
break;
case 'info' :
$form['intro']['#value'] = '<p>' . t('You have an inquiry...') . '</p>';
$form['contact_information']['#value'] = t('Send us your question. A real person will get back to you very shortly.');
$form['message']['#title'] = t('Your question');
$form['cid']['#value'] = 2;
$form['cid']['#prefix'] = '<div style="display:none;">';
$form['cid']['#suffix'] = '</div>';
$form['#prefix'] = '<div class="contact-for-general-inquiry">';
$form['#suffix'] = '</div>';
$output = drupal_render($form['intro']);
}
// dsm($form);
return $output . drupal_render($form);
}
?>
This is a contrived example of course. This is an exercise. You may wonder why in the hell you ought to add some 'intro' text field when the form already contains a 'contact_information' field. I am doing this just to show you how to add a field to a form.
To place text in a form is not exactly adding a field to it. But still, in the world of Drupal Form Theming, markup that just sits there to be read is a field in its own right. Strange, hein? Text that just sits there is even the default type for a form field in the Drupal form API. Hence, you need not specify a '#type' of 'markup' for the new $form['intro'] field. To be fair, this field (in Drupal form-speak) can consist of any markup.
Don't forget to clear the cache.
Now head over to your 'contact/quote' page.
All you have left to accomplish now is go to admin/build/menu and add menu items that link to contact/quote and contact/info, and then add these menu items to a menu, e.g. your primary links. And you're done.
Randy is happy. He sends you two-thirds of the money he promised you, along with this photo of him. He says he's on the left.
This tutorial was helpful to you? Post a comment and/or donate. Thank you.