Syndicate

Feed

Theming the contact form in Drupal 6

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:

  • recognize when a module has not registered a specific theming function for a form it generates,
  • register a theming function for a Drupal form in your theme, using HOOK_theme,
  • work with template suggestions,
  • use the Devel module function dsm() to inspect complex variables, such as Drupal forms.

Using the contact module

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.

Enable the Contact module on your Modules page.

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:

Adding a 'request a quote' category to your Contact form.

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.

Determining the cid of a contact category.

And taking 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.

Both categories on the admin/build/contact page.

Take note of the 'cid' for general inquiries.

Taking note of the cid of the general inquiries category.

What theming function to override...?

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.

The devel module must be enabled.

Make sure to move the Development block to some active region on your page.

The block must be placed in some active region on the page, such as the Right sidebar.

Once you've landed on the contact page, click on the 'Enable Theme developer' link in your Development block.

The devel block should be visible to you now.

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.)

The themer info toggle sits on the bottom-left of your viewport.

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.

Select the contact form by clicking to the right of its submit button.

There's no specific theming function that's been registered to theme the contact form. How do you know that?

The function theme_form() is used to theme the form.

How do you know that theme_form() is used to theme the contact form? The Themer info widget tells you that. Take a look.

The all-purpose theme_form function is used to theme the contact form.

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.

Registering a theming function

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.

  • Look at the <form> id attribute (using Firebug). That id happens to be the hyphenated version of your 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.
  • Look at some hidden input in the form markup with name "form_id". The value of that input is the 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.)

The Form ID of the contact form.

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:

  • When the module provides a theming function per se, a 'file' is specified, the file in which the function is defined — but the module does so only when the function is stored in a separate file, that is, not in user.module.
  • Whenever the module provides a *.tpl.php file, a 'template' is specified. The template name, as the convention goes for a form, is the hyphenated version of the Form ID. You take mental note of this.

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.

Registering a theming function for a form

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:

  • The hook_theme function always returns an array. It returns an array even when it registers only one single theming function.
  • The hook_theme function returns an associative array. In that array, the key(s) is(are) the hooks (in "themespeak") that need to be themed — and for a form, the hook is the Form ID.
  • The 'arguments' element (plural form, please!) is also an associative array, and it is an array even when it contains only one element. The key/value pair for that one element, in our case, is 'form' and NULL.
  • You could provide an 'arguments' element, and stop there. You could then use a function like garland_contact_mail_page($form) to theme the form. But if you prefer to use a *.tpl.php file to theme the form, you need to provide a 'template' name. The value of 'template' is the name of the file you intend on using — without the tpl.php extension. Note that the template name can be ANYTHING AT ALL, that is, 'anything-at-all', provided you create a file that's called anything-at-all.tpl.php. But to follow Drupal's coding conventions, you, my Drupal-worker friend, use the hyphenated version of the Form ID.

Providing a template file and its preprocess function

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:

  • Create the template file out of thin air.
  • Define a preprocess function to pass along to the template file any variable that it needs; all of the variables that have not been passed along already through the 'arguments' defined above; all of the variables it needs except the ones passed along already; all of the variables it needs excluding those variables that are automatically made available to all template files, like $user and $logged_in. (For a list of what's available in ALL *.tpl.php file, head over here).

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:

  • In $vars, the key $vars['form'] is set, as well as the keys for the default baseline variables available to all templates.
  • $vars['form'] is a complex nested array that contains our 'contact_mail_page' form.
  • $vars is passed to the preprocess function 'by reference'. This means you can add to it, edit it, do whatever you want to it, and your changes will 'keep'. The preprocess function is usually used to pass additional variables to the template file — by adding to $vars — but it can also be used to change the value of stuff that's being passed already. You'll use it that way.

You know this already about drupal_render():

  • The drupal_render() function is what's used by themes to whip a $form array into XHTML markup.
  • The drupal_render() function can render any part of a form, or the whole thing.
  • When used to render the entire form, drupal_render() will only render these parts of the form that have not been rendered yet. It can keep track of what has been rendered thus far, in other words.

Clear your Drupal cache. That will clear the Theme registry.

Using the devel block to clear the cache.

Does Drupal use my template?

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:

A test to see if our new template file is used.

Satisfied and relieved, you remove the 'So you would...' paragraph from the template file.

A little planning

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:

Three new template files in your theme folder.

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.

Checking out my path

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.

Using the devel block to clear the cache.

Then, head over to the 'contact/quote' page. What do you see? Test the 'contact/info' page as well.

Checking out the form

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']);
  ...
?>

Changing field labels

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'.)

The contact form to request a quote.

Is using template files such a great idea..?

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...

Preprocess functions are for template files only.

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.

The contact form to request a quote.

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.

Picture of Randy

[]

This tutorial was helpful to you? Post a comment and/or donate. Thank you.

Last edited by Caroline Schnapp about 12 years ago.

Comments

5 times slower?

(You heard that using a template is five times slower than using a theming function.)

Really.. 5 times slower.. It couldn't be that bad
[Or did you just want to show an alternate method].

Fantastic write up!

Regards
Alan

An alternate method

Really.. 5 times slower...

merlinofchaos said so during a screencast presentation on theming Drupal 6 theming: an overview of the changes to theming in Drupal 6 for which you can find the slides here.

Even if it'd be five times slower it would still be very quick, but that 'slowness' is the reason why Drupal will never provide templates to theme these 'hooks' that you can find by the dozens on one page. For example, you will not find a template to theme 'menu_item_link' or 'menu_item' or 'links' or 'more_link'. The rumors have it that Drupal core will provide even more templates, to copy and drop in your theme, for Drupal 7, but it won't provide templates for these hooks for sure.

But you're absolutely correct. Although I personally prefer using a function here over a template file, I _really_ wanted to show both ways. One may not perceive any difference in speed here, at all. Generally, I prefer to use a function when all that the template does is print the content of a single variable.

Fantastic write up!

Thank you :-)

Webforms

I thought I should mention in passing that there's a contributed module to create forms as nodes. After enabling the module, you have one more content type at your disposal, webform. Each time you create a 'webform', you end up with one node = one form. Each form has the attributes of forms as we know and love them, it has its own Form ID, so you can theme it, etc. And the webform is displayed in a node, so it has the attributes — and ADVANTAGES of nodes.

While you create a webform, you're presented with an E-mail to address field, so that submitted content for that form will always be sent to that an e-mail address, just like for the contact form.

You can show two of these nodes/forms on the same page like Laurence does on his contact page. Check it out: http://lhmdesign.com/contact.

The webform module has a beta version for Drupal 6, so it's all lookin' and lickin' good for its lifespan. The project page for that module is at http://drupal.org/project/webform. This module is also good for questionnaires, etc.

In about 5 minutes, I was able to install the module and create this webform:

Tell me your fantasy form.

I can enable/disable comments for it, publish/unpublish it, remove its stickiness ;-)

webform

Just tried webform as I'm busy with a site where different people have to fill in forms and it gets sent to different people... or posted to different nodes hopefully when I'm done...

Really usefull module if your not in the mood to setup CCK fields and a entire new content type for each little thing.

I'm sure you could even use this as a nice contact module replacement.

Nice contact module replacement

I'm sure you could even use this as a nice contact module replacement.

Certainly!

That's how I discovered the existence of the module. I was on a Drupal site, filling a contact form... there were 2 different contact forms on the same page, and that picked my curiousity, so I did a View Source. I saw 'webform' in the class names, and googled that.

hook_theme() is only called when cache is cleared

Just to clarify -- the garland_theme() function goes in the module's .module file, not the theme's template.php file. ALSO, this function is only called when the cache is rebuilt (so after you click "Clear Cache" from the Devel sidebar) -- it does NOT get called upon each page load. This was driving me crazy and I couldn't figure it out until I ran through everything with a step debugger.

Thanks for the great article -- this is the only place on the entire web (AND in all the drupal books) that I found an explanation for rendering a form with a template. Whatever extra load time results from doing it this way is worth it to me, because we have web designers who create HTML and hand it off to programmers -- trying to put that HTML into a php function would be a nightmare.

Thank you

Just to clarify -- the garland_theme() function goes in the module's .module file, not the theme's template.php file.

Not at all. We're not writing a module here — and you certainly should not touch core. The garland_theme() function goes in the theme's template.php file.

ALSO, this function is only called when the cache is rebuilt (so after you click "Clear Cache" from the Devel sidebar)

The tutorial is clear about this: after registering the theming function and creating the template for the contact form, I say:

Clear your Drupal cache. That will clear the Theme registry.

I even provide a screenshot.

And when I do not use a template but an overriding theming function, I still say:

Don't forget to clear the cache.

garland_theme() misprint?

I love your articles. Thank you!

Instead of returning array('user_register' => array())..., I believe the final garland_theme() function should look like:

function garland_theme(){
  return array(
    'contact_mail_page' => array(
      'arguments' => array('form' => NULL),
      // 'template' => 'contact-mail-page',
    ),
  );
}

Note: After 10 attempts to format this comment with <code> and/or <?php tags, none of my Previews looked 'pretty.' Sorry if it posts that way, too.

[EDIT by Caro] I changed the filter here, so code is tabbed. I will reply in another message...

Thank you, Rodney

I edited the tutorial.

I am so sorry about the filter... argh. I will fix this.

Comment form

I would love to see something like this for the comment form.

I tried a similar approach with the comment form, following this and the Drupaldojo video and it for some reason nothing worked.

I can call another template but cant seem to output any variables in it (it does get called and I can print a simple bit of html and message, but all the form fields fail to render). dsm outputs nothing, drupal_render($form); outputs nothing. When I use the devel theme developer it gives me no candidates for functions.

Is the comment form somehow different? how can I theme the comment for radically different than the default? I want to re-order all the fields and use a lot of additional markup.

Thanks for the great tutorial - I just wish this would work for the comment form also. I've searched high and low for advice, but no one seems to have posted any info on theming the comment for in this way as yet.

Theming and CCK forms

Hi Caroline,

Thanks for this great tutorial.

I tried your method on a CCK form and it doesn't seem to be working - cleared cached data and everything. What I did: First, I copied the garland theme to all/themes folder and renamed it myGarland - then I carried out the changes. My CCK is called front_page, so the form_id wad front_page_node_form. So I had the following in my template.php file:

function myGarland_theme() {
  return array(
    'front_page_node_form' => array(
      'arguments' => array('form => NULL'),
      'template' => 'front-page-node-form',
    ),
  );
}
function myGarland_preprocess_front_page_node_form(&$vars) {
  $vars['form']['title']['#title'] = t('My title');
  //dsm($vars['form']);
  $vars['form_markup'] = drupal_render($vars['form']);
}

and created a file called: front-page-node-form.tpl.php

Is there something else I need to do to get it to work on CCK forms?

Best regards, Remy

Questions

A few questions:

  1. Did you make the myGarland theme your new default theme?
  2. Did you guess what the form ID was, or did you check the source..?
  3. What does Themer info say now...? What function or template is used now to theme the form?
  4. I suppose uncommenting the dsm function call does not result in the form being output to screen?

Thanks for the feedback, Remy. Very appreciated!

Theming CCK forms

Hi,

Please see my responses to your questions below:

1) Yes - I did. Went through your tutorial for the contact form and that worked fine using both methods outlined.
2) Checked the form id using your method (i.e. from )
3) Themer info actually showed myGarland_front_page_node_form as candidate function names.
4) And no, uncommenting dsm function call didn't work

Hope you can help - I am about at my wits end. In case it helps, I've also logged an issue on drupal: http://drupal.org/node/316672.

regards, remy.

Themer info actually showed

Themer info actually showed myGarland_front_page_node_form as candidate function names.

Function name? It should precisely give you the name of a template to be used.

Here's what the themer info says:

Parents: theme_form < page.tpl.php
Function called:
theme_node_form()
Candidate function names:
myGarland_front_page_node_form < phptemplate_front_page_node_form < theme_front_page_node_form etc....

I looked at your support request

The code is wrong in there. You use:

function myGarland_theme() {
  return array(
    'contact_mail_page' => array(
      'arguments' => array('form' => NULL),
      //'template' => 'contact-mail-page',
      ),
  );
  return array(
    'front_page_node_form' => array(
      'arguments' => array('form' => NULL),
     //'template' => 'front-page-node-form',
    ),
  );
}

With your code you are never registering a theming function, or template, for your CCK form. Try this:

function myGarland_theme() {
  return array(
    'contact_mail_page' => array(
      'arguments' => array('form' => NULL),
      'template' => 'contact-mail-page',
    ),
    'front_page_node_form' => array(
      'arguments' => array('form' => NULL),
      'template' => 'front-page-node-form',
    ),
  );
}

Off-topic

You should perhaps avoid using capital letters in names of folders/files (and PHP function names). Bad practice that may also cause problems with Apache along the way.

found that error ... corrected it

Sorry, should have mentioned that I found that error and corrected it - didn't make any difference, unfortunately (I was very excited when I found the error - thought my troubles were over!)

Do you think perhaps using capital letter 'G' is the problem?

Not sure

Do you think perhaps using capital letter 'G' is the problem?

I don't think so.

I wish I could be more helpful.

Can you show me a screen capture of what Themer info shows for the node form? You can upload the screen capture to the Drupal support request issue.

Screenshot of theming info uploaded

Hi Caroline,

I've uploaded the screenshot of my theming info to the support issue on Drupal - http://drupal.org/node/316672. Hope you can help.

Thanks, Remy

Thanks alot

Catching up on differences of Drupal 6 and 5 for a job interview tomorrow when I came across your site and this write up. The write up is excellent both in technical clarity and entertainment. Your content and banter remind me of a more technical Annalee Newitz.

Thank you, evoltech

Your content and banter remind me of a more technical Annalee Newitz.

That is a great, great compliment.

You know your projects stand

You know your projects stand out of the herd. There is something special about them. It seems to me all of them are really brilliant!
forum-affiliation-casinos.com

How can use this function method if the form is node_form

Great tutorial, thank you very much.

I would like to format some content types that contain CCK fields in the same manner, Is that possible? I can't seem to find any unique identifier between the forms that would allow me to format one seprate from all the other content types on my site. They all appear to have the form id "node_form". Any help is appreciated.

I figured out what to call the function

I was a little premature on my previous post, I hunted around a little more and found the possible function names.

Thanks
By the way, I ran around your site for a while. You are very talented.

I have read your blog it is

I have read your blog it is very helpful for me. I want to say thanks to you. I have bookmark your site for future updates.
http://www.aflodds.com.au

Ok I have the dsm($form) output but I can't manipulate the data

Allright,

I was able to get the functions named correctly and I am able to return the dsm($form) output to the form in question, but I can't seem to override any of the vars? Here is the data from the dsm:

... (Array, 47 elements)

title (Array, 20 elements)

field_first_name (Array, 16 elements)
o
#theme (String, 23 characters ) content_multiple_values
o
#title (String, 10 characters ) First Name
o
#required (String, 1 characters ) 1
o
.....this is only a portion but it is enough for now. So the function looks like this:

function shareholders_theme(){
  return array(
    'investment_node_form' => array(
      'arguments' => array('form' => NULL),
      // 'template' => 'contact-mail-page',
    ),
  );
}
 
 
function shareholders_investment_node_form($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['investment_information']['#value'] = t('We will get back to you with a quote within 48 hours.');
      $form['field_first_name']['#title'] = t('What you need');
      //$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['investment_information']['#value'] = t('We will get back to you with a quote within 48 hours.');
      //$form['#prefix'] = '<div class="contact-for-general-inquiry">';
      //$form['#suffix'] = '</div>';
      //$output = drupal_render($form['intro']);
  } 
   dsm($form);
  return $output . drupal_render($form);
}

and I don't know why I can't override the ['field_first_name']? Any help is apreciated.

I got it

Ok well, I feel like I've taken over your comments, sorry about that. I was able to resolve the issue using the dsm($form). it turns out that the field I was testing on had PHP code in the cck default field to help pull the users name if they were authenticated. Consequently the [#title] field under the field name was being superseded by a nested ['#title'] field way down in the fields nested arrays within the default values.

Anyhow, thanks again.

Just a very minor comment,

Just a very minor comment, that dsm() is actually identical to dpm() ... see http://cvs.drupal.org/viewvc.py/drupal/contributions/modules/devel/devel.... Might be worth clarifying this in your useful table above.

I say so already, look:

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.

Yeah OK I'm splitting hairs;

Yeah OK I'm splitting hairs; it was that when I saw this table I had the feeling 2 were the same but it wasn't immediately obvious which without checking the devel.module source :O . These days dpm() seems to be the preferred form.

Cheers,

I will make it more obvious

I will make it more obvious.

You deserve a willing partner

1. You are awesome. That is not my "humble opinion," (do I really even have "humble" opinions?) but a direct observation of fact. This may be the very best tutorial I have ever encountered, anywhere, on any subject. And that's without factoring in the wit and humor. If you're not already receiving two ridiculous FT salaries, one as a Drupal guru and the other as a documentation genius, you should be.

2. In terms of hiding certain form elements without removing them from the form, wouldn't it be easier to set '#type' => 'hidden' (versus wrapping in a new div in order to set visibility:none; )?

3. Re. the trouble with positioning the 'intro' element at the top of the form, I think the underlying issue is that drupal_render() processes the $form array sequentially. So, another way to position a new element at the top of the form would be to rebuild the $form array, like so:

$new_elm =  array (
    'intro' => array (
        '#value' => '<p>' . t('You would like to request a quote...') . '</p>'
    )
);
 
$form = array_merge ($new_elm, $form);

This might be easier, especially in more complex scenarios.

4.

He says he's on the left

lofl

Best, and many thanks!

Hi Chris

If you're not already receiving two ridiculous FT salaries, one as a Drupal guru and the other as a documentation genius, you should be.

I am most definitely not. I like to write (can you tell?), even in English, which is not my mother tongue. But never made any money with that. Packt Publishing did contact me with a proposal to publish all my tutorials in book form. Packt Publishing prints its books on demand, with a low quality print. If I ever did publish anything, it would either be with a company that provides excellent technical review, and nice quality print, (and money) or I would create the PDF in InDesign and take care of the print myself, that would at least cover some of my living costs while writing.

In terms of hiding certain form elements without removing them from the form, wouldn't it be easier to set '#type' => 'hidden' (versus wrapping in a new div in order to set visibility:none; )

This is something you definitely want to do if you were to work from the module layer. All the best if it works on the theme layer. I think it probably does. If you can change the input type to hidden (to still submit the value), then your suggestion is better. I like it better anyway. Something about it being cleaner :-)

Your array_merge solution is excellent. I realize that I am not really a PHP programmer, I know the Drupal framework very well, and tend to not think of PHP-only solutions often enough... I am learning Ruby on Rails now, by the way. And I do intend to be a Ruby kung fu. Love it!

Thanks for everything, you made my day, Chris.

It's just so great to look

It's just so great to look up from my code (my shiny, precious, jealous code) and see that there are all these bright, interesting, good-hearted people out there--and some are even techies. I hope someday to A., write well in my first language and B., find the guts, energy and generosity of spirit do it in a forum that makes some contribution. I applaud and admire you for doing what I only dream idly of. (Well, not entirely idly; the truth is I entertain a fantasy that after I die some grad student will stumble upon my cache of journals, .txt docs and 'pulished=false' web pages and notice that although on the surface they appear to comprise a ceaseless and obsessive rehashing of my various self-pities, upon careful reading they are in fact the brilliant opus of a sensitive genius.)
best to you!

When I put: function

When I put:
function garland_preprocess_contact_mail_page(&$vars) {
  $vars['form_markup'] = drupal_render($vars['form']);
}
in my template.tpl file, I just get this error:
"Parse error: syntax error, unexpected T_VARIABLE in /home/lund/www/www/sites/all/themes/zen/lund/template.php on line 103"

I'm a novice trying to make this work. Thanks.

Wrong naming

Do not use “garland” in your function name, use your theme's name instead.

Make sure that there isn't such a function declared already. If there is, just integrate the code in it.

When I use the name of my

When I use the name of my theme, I get the same fatal error Unexpected T_Variable as soon as I put that function in. The function is not declared elsewhere, the site is a simple install.
My theme is a Zen subtheme.
Thanks for any ideas, I love your site and tutorials :)

But what about theming the submit button?

This is a great tutorial. I loved the comment about getting something to eat first (good advice). I'm running into trouble trying to change the text on the submit button to something reasonable.

I've created a content type "message" using CCK. It has two buttons by default at the bottom of the form: Save and Preview. I want to leave off the preview button, and change the Save button to send. I've tried changing the value of the submit button in the preproccesor function in template.php but it doesn't seem to work.

I've tried:
$vars['submit']['#value'] = t('Send'); //and
$vars['submit']['#value'] = 'Send';

but it doesn't seem to work. Do you have any suggestions?

Shouldn't this

Shouldn't this be:

$vars['form']['submit']['#value'] = t('Send'); //and
$vars['form']['submit']['#value'] = 'Send';

or

$form['submit']['#value'] = t('Send'); //and
$form['submit']['#value'] = 'Send';

Depending on either creating a template file or doing it all in your own theming function.

Matthias is correct

You will want to access $vars['form'] in the preprocess function. Also, please use the Developer module to examine the content of your form variables, that is, use dsm(). Don't shoot in the dark.

Seeing it is nailing it.

How to translate in French the 'So you would like to contact us'

Hi,

Very good tutorial, it's exactly what I would like to do except that my site is multilingual and i would need to translate, let say, the 'So you would like to contact us...' sentence in your example.

I did the following :

in the preprocess function i've added the line $vars['texte_intro' ] = t('So you would like to contact us...');

i.e:

function nicolas_architectures_preprocess_contact_mail_page(&$vars) {
$vars['texte_intro' ] = t('So you would like to contact us...');
$vars['form_markup'] = drupal_render($vars['form']);
}

and in the contact-mail-page template i simply print the texte_intro variable

<?php print $texte_intro ?>

<?php print $form_markup; ?>

I was expecting to find the sentence So you would like to contact us... in the strings to translate search section of the i18n module, but no success!

Do you have a clue on how to do this ??

N.

Thank you!

Thanks for writing this brilliant article. I've been struggling with theming the contact form in Drupal 6 for ages, and now I finally have a solution! And I love your use of handwritten note graphics as memory aids :)

Can't get the contact page to change using the tutorial

I'm new to Drupal this tutorial looks and reads great but I'm not sure where my template.php file and .tpl.php files are supposed to go. Tried them in my sites/all/themes folder and in sites/all/themes/garland and even tried dumping them into themes/garland 's template.php. Just copying and pasting exactly what you have.

Help would be awesome. I'm using Drupal 6 (just incase that was the first response) on a base install that has only had devel, admin_menu and the contact module turned on. But it doesn't show me the altered form in the first part of the tutorial.

Thanks for the help :)

What a NEWB!!!

Sorry I'm such a newb. I didn't know it was okay to copy garland theme to my themes folder. Works fine.

Thanks for the tutorial

Thanks for a great article

Amazing article. It helped a lot for me to customize the contact form. Awsome work!!!

I am a newbie to drupal. I don't know the basics of drupal and this article saved me lot of time.

Thanks
Pushparaj

You're welcome

Thanks for the positive feedback!

Problem when switching from template files to function

Hey Caroline, I really enjoyed the tutorial. From your detailed notes I can learn a lot more than the task itself. I got a problem here and I've been fighting for hours. If you could give some advice...

So everything was fine until I started to dump the template files and use function to do the job.

Here is my code to register the node ID (wowstory_node_form). Because the zen_theme() function already exists, I had to insert the code inside those irrelevant stuff.

<?php 
function zen_theme(&$existing, $type, $theme, $path) {
   return array(
    'wowstory_node_form' => array(
      'arguments' => array('form' => NULL),
	  //'template'=> 'wowstory-node-form',
    ),
  );
 
  if (!db_is_active()) {
    return array();
  }
  include_once './' . drupal_get_path('theme', 'zen') . '/template.theme-registry.inc';
  return _zen_theme($existing, $type, $theme, $path);
 
 }
?>

And here is my function:

<?php
function zen_wowstory_node_form($form) {
 switch(arg(0)) {
    case 'node' :
     $form['field_cat']['value']['#title']=t('Category');
      break;
	 case 'home' :   
     $form['field_cat']['value']['#title']=t('mamawlawla');
     $form['buttons']['submit']['#value']=t('Share');
  }
 
return  drupal_render($form);
 
}
?>

My current theme is called neuromods and am using zen as a base theme.
When they are both in template.php, my homepage can not load(other page still can open). I thought I followed every step and can't figure out why.

Ohh, also could you please share a broad concept of how to redirect a submit button? I am trying to make a submit form which can slide open/close on homepage, and when users click the submit button, I want them to see the success message in the same block, not another page. Thank you very much!

dsm() and content profile module

Hey, After reading this article I got the dsm() function working and I've loved it. However, I'm using the Content Profile module to put CCK fields into my registration forms and I can't seem to get the dsm() function to show any information about those fields. Any suggestions?

thanks + alphabetize by last names in contact form snippet

Thanks- your tutorial was exactly what I needed: Have a list of full names as the contacts, and needed to sort by last name instead of first on the contact page. This snippet seems to work:
1) edit mytheme_theme exactly as above

2)

function mytheme_contact_mail_page($form) {

// Sort "categories" based on last name
$options = $form['cid']['#options'];
$please_choose = array_shift($options); // pull out the "Please Choose" field, don't try to alphabetize

foreach ( $options as $option ) {
$pieces = explode(' ', $option );
$last = end($pieces);
$sortable[$last] = $option; // seems to sort alphabetically on it's own, was intending to use ksort
}
array_unshift($options, $please_choose); // put back "Please Choose" field

$form['cid']['#options'] = $options;
return drupal_render($form);
}

Good post, thanks. Will

Good post, thanks. Will recommend to Randy.

I themed the form to have the small fields on the left and the message field on the right, among others.

contact form for ilovebrusse.com on Twitpic

Changing the size of the input field...

Hello there,

Was so glad to find your text!

But do you have any tips on how to change the size variable of the input fields in the contact form???

Thanks,

VW

I would use CSS for this, Victoria

With CSS you control the width with pixel precision. With HTML, it's not consistent. All you need to do is edit your stylesheet...

Happy holidays, Victoria.

Somehow...

All the information I found on the Drupal website about this had to do with the hook function and modifying the size variable in the HTML. The CSS solution works just fine. Thanks a lot! And happy holidays to you too!

Victoria, a Drupal newbie

Found the Tutorial Usefull

Your Tutorial proved usefull to me.Thanks to your tutorial I have saved a considerable amount of time in theming the contact form.For first I was considering overridding theme_textfield to change the label of the contents of the form.But, this is much easier.

Thank You

Love Drupal.

I love your work and look

I love your work and look forward for more work from your side. I am a regular visitor of this site and by now have suggested many people.
custom logo mats

Brilliant tutorial

I've read a lot of coding tutorials in my time and that is possibly one of the best I've ever had the pleasure to follow. Clear, concise and witty. Thanks for making my life a little easier:)

You're welcome, Nicky!

I am trying to make my own the powerful beast that is Drupal...

do not try to bend the spoon. Use the hook.

I guess learning actually can be fun!
Really enjoyed reading the article. Thank you.

Themeing webforms?

Hi,

First; I love your blog and witty writings!

Second; I'm finding myself completely stuck right now trying to theme a webform node based on this tutorial, and wonder if there are any mayor differences in approach that I'm oblivious to.

I'm working with an extension to the Zen theme and am trying to add a hook function for a webform (form id=webform-client-form-8 from generated html-code). This isn't picked up by the theme engine, which uses the standard theme_form() and webform-form.tpl.php still. Uncommenting the .tpl file and adding a preprocess function doesn't yield results either. Cache has been cleared.

My code:

function perrongbasic_theme(&$existing, $type, $theme, $path) {
  $hooks = zen_theme($existing, $type, $theme, $path);
 
  // Add your theme hooks like this:
  /*
  $hooks['hook_name_here'] = array( // Details go here );
  */
 
  $hooks['webform_client_form_8'] = array(
        'arguments' => array('form' => NULL),
     // 'template' => 'webform-form-8-copy',
  );
 
  return $hooks;
}
 
 
function perrongbasic_webform_client_form_8($form) {
// function isn't used to generate the output.
}

Thank you,
Freddie

you did nice work your post

you did nice work your post is awesome its increase my knowledge.the post is best i can never read before this type of post nice sharing. keep it up
maestria en el extranjero

Contact form in Block

I came across the problem that i actually need a contact form in block. I tried to apply this approach to my issue but it didn't work. What i actually did was changing theme registering function to this:

function garland_theme(){
  return array(
    'contact_mail_block' => array(
      'arguments' => array('form' => NULL),
      'template' => 'contact-mail-block',
    ),
  );
}

and my preprocessor looks like this:
function acro_preprocess_contact_mail_block(&$vars) {
  $vars['form_markup'] = drupal_render($vars['form']);
}

I assume the problem is that contact module does not create a block like it does for page in its hook_menu. So i wonder is there any way to create a contact block without changing code of module.

change the default value of the field

I have to thank you for this fab article. I tried to change the #default_value of the name filed but couldn't make it. here is the code i used:

$form['name']['#default_value'] = t('Type your name here!');

Am i missing something?

with using the theme

with using the theme developer of devel i can confirm that the #default_value has the value which i set. but the value is not shown on the form field.

Theming the contact form

I've been intensely immersed in Drupal 6.X for the past 6 months and having a great time. Just wanted to say that this is THE Best, well written article on anything I have yet read with respect to Drupal. I hope you write a book.

Great Article

Great article ! Thanks.

Excellent tutorial

This is a very good Drupal tutorial, very clear and complete, thank you very much !

Theming

This is very clear and good post.Any body can easily understand a process of theming and module.

Well done!

Everything is explained in this tutorial and I was able to do a contact form on the spot. Thank you for this! massage zürich

Contact Form

Everything extremely well explained here, thank you so much for this, I got my contact form in no time at all!