I'd like to make a wizard use Ajax. I have set 'ajax' => TRUE and 'ajax render' => 'foo_callback', but the callback is never called.

Also, what should be in the callback, is it something like the following code?:

function foo_ajax(&$form_state, $output) {
  ctools_include('ajax');
  $commands[] = ctools_ajax_command_after('body', $output);
  ctools_ajax_render($commands);
}

Comments

merlinofchaos’s picture

Hmm. I have never actually tried this callback. That does look correct, though.

One thing I see in the code is taht $form_state['rerender'] needs to be TRUE in order for this to happen.

AntoniTen’s picture

I'm trying to do exactly what the OP states (down to using the same tutorial), and I must say that I'm having the same problems with AJAX. I can not get the callback to execute.

I've set, inside of the form_info array that is passed to ctools_wizard_multistep_form

 'ajax'=>true,
'ajax render'=>'mymodule_render_ajax',
'rerender'=>true, 

and I have

function mymodule_render_ajax(&$form_state,$output){
  dpm($form_state);  //want to check what gets passed, exactly
  dpm($output);
}

but it never gets called.

I have not been able to find an example of using AJAX with the form wizard. Is there any available?

merlinofchaos’s picture

Category: support » bug

Sounds like this is probably a bug.

If you could provide me a small example module that demonstrates this it'll be easier for me to reproduce and debug.

AntoniTen’s picture

I have finally found why the callback was not getting called: 'ajax render' must be defined in the $form_state (third function parameter of ctools_wizard_multistep_form) and not in $form_info (First parameter of said function). I have read the code and found that the only reference to ajax render is on line 104 > if (!empty($form_state['ajax render'])) {< that looks for it in the $form_state variable (third parameter). The Advanced Help documentation does not properly indicate this, putting "ajax render" in $form_info

Now, I'm trying to see how to write a proper callback to add ajax to the wizard. I have tried the above solution, and it simply spewed a white screen, or a bunch of json (I think that it's json, at least. I definitively recognize some parts of it as being my form)

[ { "command": "settings", "argument": { "basePath": "/", "dhtmlMenu": { "slide": "slide", "siblings": "siblings", "children": "children", "doubleclick": "doubleclick", "relativity": 0, "clone": 0 }, "lightbox2": { "rtl": "0", "file_path": "/(\\w\\w/)sites/default/files", "default_image": "/sites/all/modules/lightbox2/images/brokenimage.jpg", "border_size": "10", "font_color": "000", "box_color": "fff", "top_position": "", "overlay_opacity": "0.8", "overlay_color": "000", "disable_close_click": 1, "resize_sequence": "0", "resize_speed": 400, "fade_in_speed": 400, "slide_down_speed": 600, "use_alt_layout": 0, "disable_resize": 0, "disable_zoom": 0, "force_show_nav": 0, "loop_items": 0, "node_link_text": "View Image Details", "node_link_target": 0, "image_count": "Imagen !current de !total", "video_count": "Video !current de !total", "page_count": "Pagina !current de !total", "lite_press_x_close": "press \x3ca href=\"#\" onclick=\"hideLightbox(); return FALSE;\"\x3e\x3ckbd\x3ex\x3c/kbd\x3e\x3c/a\x3e to close", "download_link_text": "Descargar", "enable_login": false, "enable_contact": false, "keys_close": "c x 27", "keys_previous": "p 37", "keys_next": "n 39", "keys_zoom": "z", "keys_play_pause": "32", "display_image_size": "", "image_node_sizes": "()", "trigger_lightbox_classes": "", "trigger_lightbox_group_classes": "", "trigger_slideshow_classes": "", "trigger_lightframe_classes": "", "trigger_lightframe_group_classes": "", "custom_class_handler": 0, "custom_trigger_classes": "", "disable_for_gallery_lists": true, "disable_for_acidfree_gallery_lists": true, "enable_acidfree_videos": true, "slideshow_interval": 5000, "slideshow_automatic_start": true, "slideshow_automatic_exit": true, "show_play_pause": true, "pause_on_next_click": false, "pause_on_previous_click": true, "loop_slides": false, "iframe_width": 600, "iframe_height": 400, "iframe_border": 1, "enable_video": 0 } } }, { "command": "css_files", "argument": [ { "file": "/modules/node/node.css?3", "media": "all" }, { "file": "/modules/system/defaults.css?3", "media": "all" }, { "file": "/modules/system/system.css?3", "media": "all" }, { "file": "/modules/system/system-menus.css?3", "media": "all" }, { "file": "/modules/user/user.css?3", "media": "all" }, { "file": "/sites/all/modules/cck/theme/content-module.css?3", "media": "all" }, { "file": "/sites/all/modules/ctools/css/ctools.css?3", "media": "all" }, { "file": "/sites/all/modules/dhtml_menu/dhtml_menu.css?3", "media": "all" }, { "file": "/sites/all/modules/filefield/filefield.css?3", "media": "all" }, { "file": "/sites/all/modules/lightbox2/css/lightbox.css?3", "media": "all" }, { "file": "/sites/all/modules/panels/css/panels.css?3", "media": "all" }, { "file": "/sites/all/modules/selector_reductores/css/selector_reductores.css?3", "media": "all" }, { "file": "/sites/all/modules/video/types/uploadfield/uploadfield.css?3", "media": "all" }, { "file": "/sites/all/modules/views_slideshow/contrib/views_slideshow_singleframe/views_slideshow.css?3", "media": "all" }, { "file": "/sites/all/modules/cck/modules/fieldgroup/fieldgroup.css?3", "media": "all" }, { "file": "/sites/all/modules/views/css/views.css?3", "media": "all" }, { "file": "/sites/all/modules/ctools/css/wizard.css?3", "media": "all" } ] }, { "command": "scripts", "argument": [ "/misc/jquery.js?3", "/misc/drupal.js?3", "/sites/all/modules/dhtml_menu/dhtml_menu.js?3", "/sites/all/modules/lightbox2/js/lightbox.js?3", "/sites/all/modules/panels/js/panels.js?3", "/sites/all/modules/views_slideshow/js/jquery.cycle.all.min.js?3", "/sites/all/modules/views_slideshow/contrib/views_slideshow_singleframe/views_slideshow.js?3" ] }, { "command": "after", "selector": "body", "data": "\x3cdiv id=\"selector-reductores-form\"\x3e\x3cform action=\"/selector/fabricante\" accept-charset=\"UTF-8\" method=\"post\" id=\"selector-reductores-fabricante-form\"\x3e\n\x3cdiv\x3e\x3cdiv class=\"wizard-trail\"\x3e\x3cspan class=\"wizard-trail-current-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-current\"\x3eFabricante del motor\x3c/span\x3e\x3cspan class=\"wizard-trail-current-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eMotor\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eFamilia del reductor\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eRatio\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eTamaño\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eJuego\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eOpciones\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-separator\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next-before\"\x3e\x3c/span\x3e\x3cspan class=\"wizard-trail-next\"\x3eInforme\x3c/span\x3e\x3cspan class=\"wizard-trail-next-after\"\x3e\x3c/span\x3e\x3c/div\x3e\x3cdiv class=\"active field-wrapper expanded\"\x3e\x3cdiv class=\"form-item\" id=\"edit-fabricante-wrapper\"\x3e\n \x3clabel for=\"edit-fabricante\"\x3eFabricante: \x3c/label\x3e\n \x3cselect name=\"fabricante\" class=\"form-select form-item select-field active expanded\" id=\"edit-fabricante\" size=\"17\"\x3e\x3coption value=\"15\"\x3eAllen Bradley - Rockwell\x3c/option\x3e\x3coption value=\"8\"\x3eAxor\x3c/option\x3e\x3coption value=\"6\"\x3eB\x26amp;R\x3c/option\x3e\x3coption value=\"2\"\x3eControl Techniques\x3c/option\x3e\x3coption value=\"7\"\x3eFagor\x3c/option\x3e\x3coption value=\"13\"\x3eFanuc\x3c/option\x3e\x3coption value=\"17\"\x3eFuji\x3c/option\x3e\x3coption value=\"11\"\x3eLenze\x3c/option\x3e\x3coption value=\"9\"\x3eMitsubishi\x3c/option\x3e\x3coption value=\"12\"\x3eOemer\x3c/option\x3e\x3coption value=\"3\"\x3eOmron\x3c/option\x3e\x3coption value=\"5\"\x3ePanasonic\x3c/option\x3e\x3coption value=\"4\"\x3eRexroth - Bosh - Indramat\x3c/option\x3e\x3coption value=\"14\"\x3eSem\x3c/option\x3e\x3coption value=\"1\"\x3eSiemens\x3c/option\x3e\x3coption value=\"10\"\x3eYaskawa\x3c/option\x3e\x3coption value=\"16\"\x3eZZ Otro fabricante / otro motor\x3c/option\x3e\x3c/select\x3e\n\x3c/div\x3e\n\x3c/div\x3e\x3cinput type=\"hidden\" name=\"form_build_id\" id=\"form-561421aa35553198cc723692472826fd\" value=\"form-561421aa35553198cc723692472826fd\" /\x3e\n\x3cinput type=\"hidden\" name=\"form_token\" id=\"edit-selector-reductores-fabricante-form-form-token\" value=\"4e3231c2da981ae8decd0a91993370af\" /\x3e\n\x3cinput type=\"hidden\" name=\"form_id\" id=\"edit-selector-reductores-fabricante-form\" value=\"selector_reductores_fabricante_form\" /\x3e\n\x3cdiv class=\"clear-block\"\x3e\x3cinput type=\"submit\" name=\"op\" id=\"edit-next\" value=\"Siguiente\" class=\"form-submit ctools-use-ajax\" /\x3e\n\x3cinput type=\"submit\" name=\"op\" id=\"edit-cancel\" value=\"Reiniciar\" class=\"form-submit ctools-use-ajax\" /\x3e\n\x3c/div\x3e\n\x3c/div\x3e\x3c/form\x3e\n\x3c/div\x3e" } ]

AntoniTen’s picture

Status: Active » Needs review

Ok, got AJAX working with the wizard. Rather simple (Kudos merlinofchaos) but quite badly documented:

First, 'ajax'=>TRUE and 'ajax render'=>'yourmodule_ajax_render' should be set in the $form_state array, not in the $form_state array (the documentation is wrong on this). DO NOT set 'rerender' to TRUE, as it will cause problems.

In 'ajax render', you set your rendering function to whatever you need. There are no examples. Here's what I did:

I created a launcher page:

 function yourmodule_wizard_launcher(){
  ctools_include('ajax');
  drupal_add_js('misc/jquery.form.js');
  ctools_add_js('ajax-responder');
  $output='<div id="whatever"><p>'.ctools_ajax_text_button(t('Start wizard.'), 'whatever/nojs/firststep', t('Start wizard.')).'</p></div>';
  return $output;
}

Note that I added jquery.form.js to the page. Otherwise, you will not be able to submit (I think that there should be a way to have ctools handle this. What about creating ctools_wizard_add_js($type='ajax') that added jquery.form.js, ajax-responder.js and/or the modal js files, depending on the parameter.

Now, for the 'ajax render' callback, here's what I used

function yourmodule_ajax_render(&$form_state,$output){
  $messages=theme_status_messages();
  $commands[] = ctools_ajax_command_html("#whatever", $messages.$output);
  ctools_ajax_render($commands);
}

Finally, you'll need to change your cancel and finish callbacks so they call yourmodule_wizard instead of setting $form_state['redirect']

I think that that's all the changes I had to do to get it working, but I might be missing something.

merlinofchaos’s picture

Status: Needs review » Active

That makes sense. 'ajax render' really should go into the form info. Or maybe I should be flexible and allow it to be either. So this is definitely a bug. I'm glad you figured this out and got it working. Now to figure out the way it *should* be and do it in such a way that it doesn't kill your code.

As for the redirect, there is a $form_state['no_redirect'] I think that can hard prevent redirection. This is often a good idea when using AJAX.

intrafusion’s picture

Version: 6.x-1.x-dev » 6.x-1.8

I am also struggling with this, I'm trying to do a UK postcode lookup which will then populate a select list with the relevant addresses for a user to pick from, I have followed the guidance in this post and my code looks like:

<?php
function signup_build() {
	ctools_include('wizard');
	ctools_include('object-cache');
	ctools_include('ajax');
	drupal_add_js('misc/jquery.form.js');
	ctools_add_js('ajax-responder');
	
	$step = arg(2); 
	$form_info = array(
		'id' => 'signup_build',
		'path' => "sign-up/%/%step",
		'next text' => 'Proceed to Step 2',
		'next callback' => 'signup_build_add_subtask_next',
		'finish callback' => 'signup_build_add_subtask_finish',
		'order' => array(
			'advert' => t('Step 1'),
			'personal' => t('Step 2'),
		),
		'forms' => array(
			'advert' => array('form id' => 'signup_build_advert'),
			'personal' => array('form id' => 'signup_build_personal'),
		),
	);
	$form_state = array(
		'ajax' => true,
		'ajax render' => 'signup_build_ajax',
		//'rerender' => true, 
		'cache name' => NULL,
	);
	$multi_step_form = signup_build_get_page_cache(NULL);
	
	if (!$multi_step_form) {
		$step = current(array_keys($form_info['order']));
		$multi_step_form = new stdClass();
		ctools_object_cache_set('signup_build', $form_state['cache name'], $multi_step_form);
	}
	
	$form_state['signup_build_obj'] = $multi_step_form;
	$output = ctools_wizard_multistep_form($form_info, $step, $form_state);
	
	return $output;
}
?>

When I visit the page I either get a whitescreen or json like AntoniTen mentions in #4 but I can't seem to get any further, any help would be appreciated.

Ashlar’s picture

Status: Active » Closed (won't fix)

This bug report has not been active for over six months. In an effort to clean-up the issue queue this item has been closed. If your modules are current and the report is still relevant please feel free to change the Status back to active.

haggins’s picture

I'm trying to get a simple example wizard running with ajax with Ctools 7.x-1.1. Without, it's working fine. I used the way as AntoniTen suggested.
However, if I click at the ctools_ajax_text_button to start the wizard I get no response even the ajax render function gets called.

Here's my example module:

function create_wizard_menu() {
	$items['event/create'] = array(
			'title' => 'Create your event',
			'page callback' => 'create_wizard_wizard_launcher',
			'access arguments' => array('access content'),
	);
	
	$items['event/create/%'] = array(
			'title' => 'Create your event',
			'page callback' => 'create_wizard_wizard',
			'page arguments' => array(2),
			'access arguments' => array('access content'),
			'type' => MENU_CALLBACK,
	);
	
	return $items;
}

function create_wizard_render_ajax(&$form_state, $output) {
  $commands[] = ajax_command_html("#create-wizard", $output);
  ajax_render($commands);
}

function create_wizard_wizard_launcher() {
	ctools_include('ajax');
	drupal_add_js('misc/jquery.form.js');
	ctools_add_js('ajax-responder');
	$output='<div id="create-wizard"><p>'.ctools_ajax_text_button(t('Start wizard.'), 'event/create/type', t('Start wizard.')).'</p></div>';
	return $output;
}

function create_wizard_wizard($step = NULL) {
	ctools_include('ajax');
	
	$form_info = array(
			'id' => 'event_creation_wizard',
			'path' => "event/create/%step",
			'show trail' => TRUE,
			'show back' => TRUE,
			'finish callback' => 'event_creation_wizard_finish',
			'order' => array(
					'type' => t('Event type'),
					'appointment' => t('Appointment'),
			),
			'forms' => array(
					'type' => array(
							'form id' => 'event_creation_wizard_form_type'
					),
					'appointment' => array(
							'form id' => 'event_creation_wizard_form_appointment'
					),
			),
	);
	
	global $user;
	$object_id = 'event_creation_wizard_'. $user->uid;
	
	if (empty($step)) {
		// We reset the form when $step is NULL because that means they have
		// for whatever reason started over.
		create_wizard_clear_page_cache($object_id);
		$step = 'type';
	}
	
	// This automatically gets defaults if there wasn't anything saved.
	$object = create_wizard_get_page_cache($object_id);
	
	$form_state = array(
		'ajax' => TRUE,
		'ajax render' => 'create_wizard_render_ajax',
		'object_id' => $object_id,
		'object' => &$object,
	);
	
	ctools_include('wizard');
	$form = ctools_wizard_multistep_form($form_info, $step, $form_state);
	$output = drupal_render($form);
	
	
	if ($output === FALSE || !empty($form_state['complete'])) {
		$event = create_wizard_create_event($object);
	}
	
	
	if ($output === FALSE || !empty($form_state['complete'])) {
		return $event;
	}
	else if (!empty($form_state['cancel'])) {
		drupal_goto('event/create');
	}
	
	return $output;
}


/**
 * Get the cached changes to a given task handler.
 */
function create_wizard_get_page_cache($id) {
    ctools_include('object-cache');
    $object = ctools_object_cache_get('create_wizard', $id);
    if (!$object) {
		// Create a default object.
        $object = new stdClass;
        $object->type = 'unknown';
    }

    return $object;
}

/**
 * Store changes to a task handler in the object cache.
 */
function create_wizard_set_page_cache($id, $object) {
	ctools_include('object-cache');
	ctools_object_cache_set('create_wizard', $id, $object);
}

/**
 * Remove an item from the object cache.
 */
function create_wizard_clear_page_cache($id) {
	ctools_include('object-cache');
	ctools_object_cache_clear('create_wizard', $id);
}


function event_creation_wizard_next(&$form_state) {
	// Update the cache with changes.
	create_wizard_set_page_cache($form_state['object_id'], $form_state['object']);
}

function event_creation_wizard_finish(&$form_state) {
	$form_state['complete'] = TRUE;
}

function event_creation_wizard_cancel(&$form_state) {
	$form_state['cancel'] = TRUE;
}

function event_creation_wizard_form_type($form, &$form_state) {
	$form['type'] = array(
		'#type' => 'radios',
		'#title' => t('Select your events type'),
		'#options' => array(
			'concert' => t('Concert'),
			'festival' => t('Festival'),
			'party' => t('Party'),
			'other' => t('Other'),
		),
		'#default_value' => $form_state['object']->type,
	);
	
	return $form;
}

function event_creation_wizard_form_type_submit($form, &$form_state) {
	$form_state['object']->type = $form_state['values']['type'];
}

function event_creation_wizard_form_appointment($form, &$form_state) {
	$form['appointment'] = array(
		'#type' => 'textfield',
		'#title' => t('Appointment'),
	);
	
	return $form;
}

function event_creation_wizard_form_appointment_submit($form, &$form_state) {
	$form_state['object']->appointment = $form_state['values']['appointment'];
}

function create_wizard_create_event($object) {
	return t('Your event is a @type at @appointment.', array('@type' => $object->type, '@title' => $object->appointment));
}

Any ideas?

haggins’s picture

Version: 6.x-1.8 » 7.x-1.0-rc2
Status: Closed (won't fix) » Active

The problem seems to be that ctools_wizard_multistep_form($form_info, $step, $form_state) returns an empty string as "ajax" and/or "ajax render" are set in $form_state.

So, is this a bug or did I miss something?

haggins’s picture

Ok, well - the render function needs to return the rendered form instead of just calling 'ajax_render()' as 'ctools_wizard_multistep_form($form_info, $step, &$form_state)' returns '$form_state['ajax render']($form_state, $output);'.

So far so good. By clicking at my "Start Wizard" ajax link, I get the first step rendered within the targeted container. However, by going to the second step, I only get JSON on my screen (as the wizard-function returns 'print ajax_render($commands);', see:

function create_wizard_wizard($step = NULL) {
	ctools_include('ajax');
	
	$form_info = array(
			'id' => 'event_creation_wizard',
			'path' => "event/create/%step",
			'ajax' => TRUE,
			'show trail' => TRUE,
			'show back' => TRUE,
			'finish callback' => 'event_creation_wizard_finish',
			'order' => array(
					'type' => t('Event type'),
					'appointment' => t('Appointment'),
			),
			'forms' => array(
					'type' => array(
							'form id' => 'event_creation_wizard_form_type'
					),
					'appointment' => array(
							'form id' => 'event_creation_wizard_form_appointment'
					),
			),
	);
	
	global $user;
	$object_id = 'event_creation_wizard_'. $user->uid;
	
	if (empty($step)) {
		// We reset the form when $step is NULL because that means they have
		// for whatever reason started over.
		create_wizard_clear_page_cache($object_id);
		$step = 'type';
	}
	
	// This automatically gets defaults if there wasn't anything saved.
	$object =create_wizard_get_page_cache($object_id);
	
	$form_state = array(
		'ajax' => TRUE,
		'ajax render' => 'create_wizard_render_ajax',
		'no_redirect' => TRUE,
// 		'rerender' => TRUE,
		'commands' => array(),
		'object_id' => $object_id,
		'object' => &$object,
	);
	
	ctools_include('wizard');
	$output = ctools_wizard_multistep_form($form_info, $step, $form_state);

	
	if ($output === FALSE || !empty($form_state['complete'])) {
		$event = create_wizard_create_event($object);
	}
	
	$commands = array();
	if ($output === FALSE || !empty($form_state['complete'])) {
		$commands[] = ajax_command_html('#create-wizard', $event);
	}
	else if (!empty($form_state['cancel'])) {
		// If cancelling, return to the activity.
	}
	else {
		$commands[] = ajax_command_html('#create-wizard', $output);
	}
	print ajax_render($commands);
	exit;
}

It seems like the wizard doesn't know that it should use ajax to progress. Even setting 'ajax' => TRUE in the $form_info array doesn't have any effect. So what's the way to tell the wizard to use ajax?

blackdog’s picture

Version: 7.x-1.0-rc2 » 7.x-1.x-dev
Category: bug » support

It would be awesome if someone who actually has this working (Wizard AJAX without modal), wrote a guide, 'cause this is tricky stuff. Can it actually be done?
I have, like the above comment, tested a whole bunch of settings and includes, but nothing seems to do the trick.

mustanggb’s picture

Issue summary: View changes
Status: Active » Closed (outdated)