September 20, 2010

Multi-step forms using Form API and Ctools

Today we will talk Form API. The API allows to expand your Drupal installation with highly extendable and secure forms. In this particular post I will show how to create a multi-step survey with file upload ability. Form API is part of Drupal's core and there's no need to download it.

To make it easier to implement multi-step forms we'll use Chaos tool suite, an additional set of API's made to further streamline developing.

All code will reside in a custom module. For the purpose of this tutorial let's call it multi_step_form.

Create a new directory, sites/all/modules/custom/multi_step_form. In it, create multi_step_form.info and multi_step_form.module files, the absolute minimum required for a proper Drupal module.

multi_step_form.info contents:

name = Multi Step Form description = Multi Step Form core = 6.x 

The rest of the code will be done in multi_step_form.module.

The module begins with the implementation of hook_menu().

'Multi Step Form Wizard', 'page callback' => 'multi_step_form_wizard', 'access arguments' => array('access content') ); return $items; } 

Setting up the form with Ctools.

function multi_step_form_wizard() { 
	ctools_include('wizard'); 
	ctools_include('object-cache'); 
	$step = arg(1); 
	$form_info = array( 
		'id' => 'multi_step_form_basic', 
		'path' => "multi_step_form/%step", 
		'show trail' => true, 
		'show back' => true, 
		'show cancel' => true, 
		'show return' => false, 
		'next text' => 'Proceed to next step', 
		'next callback' => 'multi_step_form_basic_add_subtask_next', 
		'finish callback' => 'multi_step_form_basic_add_subtask_finish', 
		'return callback' => 'multi_step_form_basic_add_subtask_finish', 
		'cancel callback' => 'multi_step_form_basic_add_subtask_cancel', 
		'order' => array( 
			'address' => t('Step 1'), 
			'comment' => t('Step 2')
		), 
		'forms' => array( 
			'form_details' => array( 
				'form id' => 'multi_step_form_address'
			), 
			'company_production_facility_address' => array( 
				'form id' => 'multi_step_form_comment' 
			)
		) 
	); 

	$form_state = array( 
		'cache name' => NULL
	); 

	$multi_step_form = multi_step_form_basic_get_page_cache(NULL); 

	if (!$multi_step_form) { 
		$step = current(array_keys($form_info['order'])); 
		$multi_step_form = new stdClass(); 
		ctools_object_cache_set('multi_step_form_basic', $form_state['cache name'], $multi_step_form); 
	};

	$form_state['multi_step_form_obj'] = $multi_step_form; $output = ctools_wizard_multistep_form($form_info, $step, $form_state); 

	return $output; 
};

We included the needed Ctools, set basic multi-step form parameters, and added two steps.

Now let's define the actual steps. Each one will require a form defined using Form API format, a function to validate, and a function to submit. Let's create a basic form that captures addresses.

function multi_step_form_address(&$form, &$form_state) { 
	$r = array(
		' ',
		'Alberta',
		'British Columbia',
		'Manitoba',
		'New Brunswick',
		'Newfoundland and Labrador',
		'Nova Scotia',
		'Northwest Territories',
		'Nunavut',
		'Ontario',
		'Prince Edward Island',
		'Quebec',
		'Saskatchewan',
		'Yukon',
		'Alabama',
		'Alaska',
		'Arizona',
		'Arkansas',
		'California',
		'Colorado',
		'Connecticut',
		'Delaware',
		'District of Columbia',
		'Florida',
		'Georgia',
		'Hawaii',
		'Idaho',
		'Illinois',
		'Indiana',
		'Iowa',
		'Kansas',
		'Kentucky',
		'Louisiana',
		'Maine',
		'Maryland',
		'Massachusetts',
		'Michigan',
		'Minnesota',
		'Mississippi',
		'Missouri',
		'Montana',
		'Nebraska',
		'Nevada',
		'New Hampshire',
		'New Jersey',
		'New Mexico',
		'New York',
		'North Carolina',
		'North Dakota',
		'Ohio',
		'Oklahoma',
		'Oregon',
		'Pennsylvania',
		'Rhode Island',
		'South Carolina',
		'South Dakota',
		'Tennessee',
		'Texas',
		'Utah',
		'Vermont',
		'Virginia',
		'Washington',
		'West Virginia',
		'Wisconsin',
		'Wyoming'
	); 

	foreach ($r as $state): 
		$states[$state] = $state; 
		$multi_step_form = &$form_state['multi_step_form_obj']; 
		$form['street_address'] = array( 
			'#type' => 'textfield', 
			'#required' => 1, 
			'#title' => 'Address', 
			'#default_value' => $multi_step_form->street_address 
		); 

		$form['city'] = array( 
			'#type' => 'textfield', 
			'#required' => 0, 
			'#title' => 'City', 
			'#default_value' => $multi_step_form->city
		); 

		$form['province_state'] = array( 
			'#type' => 'select', 
			'#default_value' => $multi_step_form->province_state, 
			'#required' => 0, 
			'#title' => 'Province/State', 
			'#options' => $states 
		); 

		$form['postal_code_zip'] = array( 
			'#type' => 'textfield', 
			'#required' => 0, 
			'#title' => 'Postal Code/Zip', 
			'#size' => 6, 
			'#maxlength' => 6, 
			'#default_value' => $multi_step_form->postal_code_zip
		); 

		$form_state['no buttons'] = TRUE; 
	endforeach;
}; 

function multi_step_form_address_validate(&$from, &$form_state) { 
	if ($form_state['values']['street_address'] == '') { 
		form_set_error('street_address', 'Your street address can\'t be empty!'); 
	}; 
}; 

function multi_step_form_address_submit(&$from, &$form_state) { 
	$submitted = $form_state['values']; 
	$save_values = array(
		'street_address',
		'city',
		'province_state',
		'postal_code_zip'
	); 

	foreach ($save_values as $value):
		$form_state['multi_step_form_obj']->$value = $submitted[$value];
	endforeach;
}; 

You will surely want to modify the fields. Consult the reference along the way. The form will capture basic address and check whether the street address field is not empty. It then stores the information in the form state object.

Now let's add another step with a basic text area to store comments.

function multi_step_form_comment(&$form, &$form_state) { 
	$multi_step_form = &$form_state['multi_step_form_obj']; 

	$form['form_comment'] = array( 
		'#type' => 'textarea',
		'#required' => 1,
		'#title' => 'Comment',
		'#default_value' => $multi_step_form->form_comment
	); 

	$form_state['no buttons'] = TRUE; 
}; 

function multi_step_form_comment_validate(&$from, &$form_state) { 
	if ($form_state['values']['form_comment'] == '') { 
		form_set_error('form_comment', 'Please leave a comment.'); 
	}; 
};

function multi_step_form_comment_submit(&$from, &$form_state) { 
	$submitted = $form_state['values']; 

	$save_values = array(
		'form_comment'
	); 

	foreach($save_values as $value):
	 $form_state['multi_step_form_obj']->$value = $submitted[$value]; 
	endforeach; 

};

And now some finishing touches. I decided to store form contents in a node. In order to do so, I created a new content type with matching CCK fields.

function multi_step_form_basic_add_subtask_finish(&$form_state) { 
	$multi_step_form = &$form_state['multi_step_form_obj']; 
	drupal_set_message('Your multi-step form has been successfully submitted!'); 
	$node = new StdClass(); 
	$node->type = 'application'; 
	$node->status = 1; 
	$node->uid = 1; 
	$node->title = $multi_step_form->street_address; 
	$node->field_street_address[0]['value'] = $multi_step_form->street_address; 
	$node->field_city[0]['value'] = $multi_step_form->city; 
	$node->field_province_state[0]['value'] = $multi_step_form->province_state; 
	$node->field_postal_code_zip[0]['value'] = $multi_step_form->postal_code_zip; 
	$node->field_form_comment[0]['value'] = $multi_step_form->form_comment; 
	node_save($node); 
	ctools_object_cache_clear('multi_step_form_basic', $form_state['cache name']); 
	$form_state['redirect'] = 'multi_step_form'; 
	drupal_goto('thank-you'); 
};

function multi_step_form_basic_add_subtask_next(&$form_state) { 
	$multi_step_form = &$form_state['multi_step_form_obj']; 
	$cache = ctools_object_cache_set('multi_step_form_basic', $form_state['cache name'], $multi_step_form); 
};

function multi_step_form_basic_add_subtask_cancel(&$form_state) { 
	ctools_object_cache_clear('multi_step_form_basic', $form_state['cache name']); 
	$form_state['redirect'] = 'multi_step_form'; 
	drupal_set_message('Multi-step form cancelled.'); 
};

function multi_step_form_basic_clear_page_cache($name) { 
	ctools_object_cache_clear('multi_step_form_basic', $name); 
};

function multi_step_form_basic_get_page_cache($name) { 
	$cache = ctools_object_cache_get('multi_step_form_basic', $name); 
	return $cache; 
};

If everything goes smoothly, the user should be forwarded to the thank-you page and the information stored in a new node.

Form API offers great ways to capture user information. Coupled with Ctools, the end result can be improved even further.