Drupal Form with dynamically adding a collection of fields via ajax

Posted on: October 25th, 2013 by admin No Comments

I put this together by combining a couple of different examples from the examples module but wanted to keep it here for future reference.

This example shows a holiday form where each holiday has a start date, end date and number of days field, and there can be multiple holidays.

1. Set up the page:

function holidays($account) {
 
	drupal_add_js(drupal_get_path('theme', 'moto').'/js/holidays.js');
 
	// Only pass holidays in the future to the holidays form
	$existing_holidays = array();
	$x=1;
	if(isset($account->field_holidays['und'])) {
 
		$length_1_day_hrs = 1439 / 60;
 
		foreach($account->field_holidays['und'] as $k => $v) {
 
			if($v['value'] > strtotime('now')) {
 
				$num_days = 0;
				$holiday_length_hrs = ($v['value2'] - $v['value']) / 60 / 60;
 
				if($holiday_length_hrs==4) {
					$start_time = date('H:i', $v['value']);
					if($start_time=='09:00') $num_days = '0.5 Days AM';
					else $num_days = '0.5 Days PM';
				} else {
					$num_days = round($holiday_length_hrs / $length_1_day_hrs);
				}
 
				// If half day then just show start date not end date
				if($num_days=='0.5 Days AM' || $num_days=='0.5 Days PM') $v['value2'] = '';
 
				$existing_holidays[$x] = array(
					'start_date' 	=> $v['value'],
					'end_date'		=> $v['value2'],
					'num_days'		=> $num_days
				);
				$x++;
			}
 
		}
 
	}
 
	//pdie($existing_holidays);
 
	return drupal_get_form('holidays_form', $existing_holidays, $account);
 
}

2. Set up the form:

function holidays_form($form, &$form_state) {
 
	$existing_holidays = $form_state['build_info']['args'][0];
	$form_state['user'] = $form_state['build_info']['args'][1];
 
	$form_state['num_holidays'] = count($existing_holidays);
 
	// We will have many fields with the same name, so we need to be able to access the form hierarchically.
	$form['#tree'] = TRUE;
 
	$form['description'] = array(
		'#type' => 'item',
		'#title' => t('Set your holidays'),
	);
 
	if (empty($form_state['num_holidays'])) {
		$form_state['num_holidays'] = 1;
	}
 
	$form['holidays_fieldset'] = array(
		'#type' => 'fieldset',
		'#title' => t('Holidays'),
		// Set up the wrapper so that AJAX will be able to replace the fieldset.
		'#prefix' => '<div id="holidays-fieldset-wrapper">',
		'#suffix' => '</div>',
	);
 
	// Build the number of name fieldsets indicated by $form_state['num_holidays']
 
	$num_holidays_to_remove = isset($form_state['holidays_to_remove']) ? count($form_state['holidays_to_remove']) : 0;
 
	$x=1;
	for ($i = 1; $i <= $form_state['num_holidays']; $i++) {
 
		if(isset($form_state['holidays_to_remove']) && in_array($i, $form_state['holidays_to_remove'])) continue;
 
		//pdie($i, $existing_holidays[$i], $existing_holidays);
 
		$form['holidays_fieldset']['holiday'][$i] = array(
			'#type' => 'fieldset',
			'#title' => t('Holiday #@num', array('@num' => $i)),
			'#collapsible' => TRUE,
			'#collapsed' => FALSE,
		);
		$form['holidays_fieldset']['holiday'][$i]['start_date'] = array(
			'#type' => 'date_popup',
			'#title' => t('Start date'),
			'#date_format' => 'd/m/Y',
    		'#date_year_range' => '-0:+2',
			'#required' => FALSE,
			'#default_value' => isset($existing_holidays[$i]['start_date']) ? date('Y-m-d H:i:s', $existing_holidays[$i]['start_date']) : '',
		);
		$form['holidays_fieldset']['holiday'][$i]['end_date'] = array(
			'#type' => 'date_popup',
			'#title' => t('End date'),
			'#date_format' => 'd/m/Y',
    		'#date_year_range' => '-0:+2',
		);
		if(isset($existing_holidays[$i]['end_date']) && $existing_holidays[$i]['end_date']!='') {
			$form['holidays_fieldset']['holiday'][$i]['end_date']['#default_value'] = date('Y-m-d H:i:s', $existing_holidays[$i]['end_date']);
		}
		$form['holidays_fieldset']['holiday'][$i]['num_days'] = array(
			'#type' => 'select',
			'#title' => t('or, Number Of Days'),
			'#options' => array(
				'0' 			=> 'Select Days...',
				'0.5 Days AM' 	=> '0.5 Days AM',
				'0.5 Days PM' 	=> '0.5 Days PM',
				'1' 			=> '1 Day',
				'2' 			=> '2 Day',
				'3' 			=> '3 Day',
				'4' 			=> '4 Day',
				'5' 			=> '5 Day',
				'6' 			=> '6 Day',
				'7' 			=> '7 Day',
				'8' 			=> '8 Day',
				'9' 			=> '9 Day',
				'10' 			=> '10 Day',
				'11' 			=> '11 Day',
				'12' 			=> '12 Day',
				'13' 			=> '13 Day',
				'14' 			=> '14 Day',
			),
			'#default_value' => isset($existing_holidays[$i]['num_days']) ? $existing_holidays[$i]['num_days'] : '0',
		);
 
		if($form_state['num_holidays']-$num_holidays_to_remove > 1) {
			$form['holidays_fieldset']['holiday'][$i]['remove_holiday'] = array(
				'#type' => 'submit',
				'#value' => t('Remove holiday '.$i),
				'#submit' => array('holidays_form_remove_holiday'),
				'#limit_validation_errors' => array(),
				'#ajax' => array(
			      'callback' => 'holidays_form_ajax_callback',
			      'wrapper' => 'holidays-fieldset-wrapper',
			    ),
			);
		}
 
 
		$x++;
	}
 
	// Adds "Add another holiday" button
	$form['holidays_fieldset']['add_holiday'] = array(
		'#type' => 'submit',
		'#value' => t('Add another holiday'),
		'#submit' => array('holidays_form_add_holiday'),
		'#ajax' => array(
	      'callback' => 'holidays_form_ajax_callback',
	      'wrapper' => 'holidays-fieldset-wrapper',
	    ),
	);
 
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => 'Save',
	);
 
	return $form;
}

3. Set up ajax callback handler to return the correct part of the form:

function holidays_form_ajax_callback($form, $form_state) {
  return $form['holidays_fieldset'];
}

4. Set up the function to handle adding new holidays:

function holidays_form_add_holiday($form, &$form_state) {
	// Everything in $form_state is persistent, so we'll just use
	$form_state['num_holidays']++;
 
	$form_state['build_info']['args'][0][] = array();
 
	// Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
	$form_state['rebuild'] = TRUE;
}

5. Set up the function to handle removing holidays:

function holidays_form_remove_holiday($form, &$form_state) {
 
	$form_state['holidays_to_remove'][] = str_replace('Remove holiday ', '', $form_state['values']['op']);
 
	// Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
	$form_state['rebuild'] = TRUE;
}

6. Validate the form:

function holidays_form_validate($form, &$form_state) {
 
	foreach($form_state['values']['holidays_fieldset']['holiday'] as $k => $values) {
 
		if($values['start_date']=='' && $values['end_date']=='' && $values['num_days']==0) {
			// there are no values so assume that this holiday is being deleted
		} else {
			if(!$values['end_date'] && !$values['num_days']) {
				form_set_error("holidays_fieldset][holiday][$k][end_date", t('Please Select End Date or Number of Days'));
				form_set_error("holidays_fieldset][holiday][$k][num_days", '');
			}
 
			if($values['start_date'] && $values['end_date'] && !$values['num_days']) {
				if(strtotime($values['start_date']) > strtotime($values['end_date'])) {
					form_set_error("holidays_fieldset][holiday][$k][start_date", t('Start date cannot be after the end date'));
					form_set_error("holidays_fieldset][holiday][$k][end_date", '');
				}
			}
 
			if(strtotime($values['start_date']) < strtotime('now')) {
				form_set_error("holidays_fieldset][holiday][$k][start_date", t('Start must be in the future'));
			}
		}
 
	}
 
}