CTools Modal Windows - Part III

Posted on 09/28/12

In the earlier parts of this series, I've shown you have to create a simple modal popup using the CTools framework, and I've explained a bit about how Drupal renders the modal. Now its time to get our hands dirty - let's put a form in our modal window. We'll put the node/add/article forum in a modal window.

We've already used hook_menu() to setup a callback for our modal, let's change it just a smidgen

<?php
function mymodule_menu() {

 
$items['modal-test-callback/%ctools_js'] = array(
   
'page callback' => 'mymodule_modal_callback',
   
'page arguments' => array(1),
   
'access arguments' => array('create article content'),
   
'type' => MENU_CALLBACK,
  );

  return
$items;
}
?>

What's new here? First off, we've added a second argument to the url, %ctools_js. Drupal takes any piece of a menu path that begins with a % sign and passes it to an 'autoload function' by the same name. So for example, if a hook_menu() item has the path 'node/%node', and a user goes to the path 'node/1', 1 will be passed to the node_load() function, and the result will be passed in as an argument to the item's page callback. In our case, the second argument is passed to ctools_js_load(), which will help us to determine if the user has javascript enabled or not. If a user goes to the path 'modal-test-callback/nojs', the ctools_js_load() function will return fase. However, if a user goes to 'modal-test-callback/ajax', it will return true. There's also new page and access arguments; I'll leave you to the hook_menu() documentation to interpret those.

Let's review the code we used to create our module's defined block, with one small adjustment.

<?php
function mymodule_block_info() {
 
$blocks['modal_test'] = array(
   
'info' => t('Modal Test Block'),
  );
  return
$blocks;
}

function
mymodule_block_view($block_name) {
  if (
$block_name == 'modal_test') {
   
   
ctools_include('modal');
   
ctools_include('ajax');
   
ctools_modal_add_js();
   
    return array(
     
'subject' => t('Modal Test Block Title!'),
     
'content' => ctools_modal_text_button(t('Click Here!'),'modal-test-callback/nojs',t('Click Here!')),
    );
  }
}
?>

See the previous articles in this series for explanations of these two functions. The one change I've made is the path in ctools_modal_text_button(). Notice the 'nojs' that has been included. This 'nojs' will be re-written by the ctools javascript, which will replace it with 'ajax'. It's our job to use this re-writing and the ctools_js_load() autoloader to make sure our site is accessible to users without javascript (i.e. screenreaders, etc).

Let's make some changes to our mymodule_modal_callback() so that it will bring up the article node form. I'll provide the full callback here, and then we'll dissect its pieces below.

<?php
function mymodule_modal_callback($js = false) {
 
// If the user doesn't have javascript, redirect them to the normal node/add/article page
 
if (!$js) {
   
drupal_goto('node/add/article');
  }
  else {
   
// Javascript is on, prepare ctools modals.
   
ctools_include('ajax');
   
ctools_include('modal');

   
// Pull in the global user, and prepare a blank node to pass to the node
    // add form.
   
global $user;
   
$node = (object) array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => 'article', 'language' => LANGUAGE_NONE);
   
$node->title = NULL;
   
node_object_prepare($node);

   
// Add the node.pages.inc so that functions from the form can be used.
   
module_load_include('inc', 'node', 'node.pages');

   
// Prepare the form state, ctools reqruies ajax / title.  The node add form
    // requires node.
   
$form_state = array(
     
'ajax' => true,
     
'title' => t('Add a new Article'),
     
'node' => $node,
    );

   
// Do the ctools_modal_form_wrapping of the node form.  Returns a set of
    // ajax commands in output.
   
$output = ctools_modal_form_wrapper('article_node_form', $form_state);

    if (!empty(
$form_state['executed'])) {

     
// Add the responder javascript, required by ctools
     
ctools_add_js('ajax-responder');

     
// Create ajax command array, dismiss the modal window.
     
$output = array();
     
$output[] = ctools_modal_command_dismiss();
    }

    print
ajax_render($output);
    exit;
  }
}
?>

Let's start with the top of the function.

<?php
function mymodule_modal_callback($js = false) {
 
// If the user doesn't have javascript, redirect them to the normal node/add/article page
 
if (!$js) {
   
drupal_goto('node/add/article');
  }
  else {
    ...
  }
?>

The single argument that we declared in our hook_menu() implementation gets passed through the ctools_load_js() autoload function, and comes in as a $js boolean. We check $js, and it's false, we send the user on their merry way to the regular node/add form. If the user has javascript, we'll do the fun modal stuff.

<?php
// Javascript is on, prepare ctools modals.
   
ctools_include('ajax');
   
ctools_include('modal');

   
// Pull in the global user, and prepare a blank node to pass to the node
    // add form.
   
global $user;
   
$node = (object) array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => 'article', 'language' => LANGUAGE_NONE);
   
$node->title = NULL;
   
node_object_prepare($node);

   
// Add the node.pages.inc so that functions from the form can be used.
   
module_load_include('inc', 'node', 'node.pages');
?>

These few lines are all prep for the heavy lifting below. The ctools_include() calls are always needed to use some of the CTools' modal functions. All of this node business comes right out of Drupal's core form handling for node forms; the form needs to be initialized with an empty node. Lastly, the node.pages.inc is included so that any functions that the node/add/form needs are available.

<?php
// Prepare the form state, ctools reqruies ajax / title.  The node add form
    // requires node.
   
$form_state = array(
     
'ajax' => true,
     
'title' => 'Add a new Person',
     
'node' => $node,
    );

   
// Do the ctools_modal_form_wrapping of the node form.  Returns a set of
    // ajax commands in output.
   
$commands = ctools_modal_form_wrapper('article_node_form', $form_state);

    if (!empty(
$form_state['executed'])) {

     
// Add the responder javascript, required by ctools
     
ctools_add_js('ajax-responder');

     
// Create ajax command array, dismiss the modal window.
     
$commands = array();
     
$commands = ctools_modal_command_dismiss();
    }

    print
ajax_render($commands);
    exit;
?>

This is where the real CTools work is done. First, we prepare a $form_state array for the form. CTools requires that we set 'ajax' => true, and that we give the form a title. This title will be along the top of the modal window. The $node is passed in here, as its required by the node_form itself. We pass the name of the form we want to modalize, along with our $form_state we just built to ctools_modal_form_wrapper(). This function is a lot like drupal_get_form, but it does a lot more. It turns the form into an array of AJAX commands that will pull up the modal and render the form within it. It also wraps the form's usual behavior in special handling (i.e. not redirecting when the form is complete, error handling, etc).

If the $form_state['executed'] value isn't set, in other words, if the form has not yet been submitted, the $commands are passed directly ajax_render() and returned as JSON to be rendered by Drupal's usual AJAX handling. If the form has been executed, we return a different set of AJAX commands. In this case, we simply dismiss the modal, but we could do something cooler if we wanted. Like this:

<?php
  $output
= array();
 
$output[] = ctools_modal_command_dismiss();
 
$output[] = ajax_command_after('.ctools-use-modal',theme('status_messages'));
?>

This will output Drupal's usual messages right after the ctools modal button (using a jquery style selector). I invite you to play around with other ajax commands. For example, you could insert the newly created node on the page somewhere.

Whew, that was a pretty long explanation, and admittedly, the code is a little complex. Get some practice with using these modal windows, and you'll be able to spin them up in no time.

Comments

Moe
1/09/13 10:22 am

Really awesome guide!!! I spent hours looking for a detailed explanation about how CTools works and here is a step by step guide... Nice work! It works perfectly ;)

Ayesh
4/04/13 2:12 pm

One of the best tutorials about ctools I've ever seen. Thanks a lot for the very helpful info. Keep it up!

Kitt2012
4/20/13 1:44 am

This is a wonderful explanation.
Thank you for taking the time to write it. I only wish I had found it earlier and saved myself the 3days of repeated cache clearing, uploading and testing! Such is life.

I thought I would add a little piece that could come up for your readers in the future.
The excellent ctools function ctools_modal_form_wrapper(), as you describe acts like drupal_get_form().
The difference being the arguments you can seemingly send to your form builder function. drupal_get_form(), allows these arguments to be passed as a part of the function. What is does, is cunningly hide the fact they are actually passed inside $formstate['build_info']['args'].
So when using ctools_modal_form_wrapper(), although argument passing seems to be missing, we actually pass them the same way. First by declaring them in $form_state and then passing $form_state to ctools_modal_form_wrapper().

drupal_get_form('my_form', $my_arg);

$formstate['build_info']['args'] = array($my_arg);
ctools_modal_form_wrapper('my_form',$form_state);

Happy cache clearing.
xK

mkadin
4/20/13 2:23 pm

Good point...I've learned that little trick since originally authoring this post...it's a good one! Thanks!

Bryan Jiencke
5/06/13 11:15 am

Hey Kitt

Thanks so much for posting your follow up. It saved me a lot of research time -- worked exactly as you said.

Bryan

Kitt
6/09/13 4:38 am

No problem Bryan.
In my experience with Drupal, there are many wonderful and effective ways to achieve what you need. Largely they are not documented that well, which is a shame.
These types of blogs that Mike has written here are the life blood that keeps us all keeping up and keeping on.

An as you can see, I find myself revisiting them often for a recap or two!

Hats off to you Mike once again.

xK

Kieran
6/20/13 10:51 am

Mike,

Kept getting an error when showing the form. Error that node_form() is missing third parameter. Declaration of node_form:

function node_form($form, &$form_state, $node)

After much chasing down, this made the error message go away:

$form_state = array(
'ajax' => true,
'title' => t('Add a new Article'),
'node' => $node,
);
//KRM.
$form_state['build_info']['args'][] = $node;

The args gets merged with $form and $form_state, and used to call node_form.

Second issue: Now the node is created, but the modal does not close itself. Time to work on that one.

Kieran

Kieran
6/20/13 11:10 am

Found the problem wi' the modal form not closing. 'Tis a bug in the jquery_update module. I'll try to submit a patch, if I can work out how.

Kitt
8/23/13 1:27 am

Hey Mike,

Thought I'd add in my findings.

Loading forms from other inc files...
If these forms have ajax enabled gubbins on them, the system/ajax path will not allow them to process and throw a call to undefined validate error.
Therefore we include them, yes?

As per your example above with the node inc pages. However I have found that if we are rebuilding forms and mucking around with data etc, ctools_modal_form_wrapper() need to know about these changes inside of the $form_state. Plus all the changes that may have happened already.

The solution I found was to use the magic of
form_load_include($form_state, 'inc', 'mymodule', 'myfile');

and then set
$form_state['cache'] = TRUE;

So in full...
$formbuilder = 'myform';

if ($js) {

ctools_include('ajax');
ctools_include('modal');

$form_state = array(
'ajax' => TRUE,
'title' => $title,
'referrer' => $referrer,
'build_info' => $args,
);
form_load_include($form_state, 'inc', 'mymodule', 'myfile');
$form_state['cache'] = TRUE;

$commands = ctools_modal_form_wrapper($formbuilder, $form_state);

if (!empty($form_state['executed'])) {

bla bla ....

Enjoy that.

xK

Thanks for your marvelous posting! I certainly enjoyed reading
it, you can be a great author. I will be sure to bookmark
your blog and will eventually come back very soon.
I want to encourage you to continue your great posts, have a nice holiday weekend!

Sergey
1/15/14 8:35 am

Cool article! Thanks!

Manage everything with our free online accounting service Be mindful if the business group is smallSo this is the query, what are your plans for marketing, advertising and promoting your new business? Once you've run out of folks michael kors you know to market the product to, what will you do next? Do you actually have any marketing or sales experience? Have you any idea about how to market the michael kors outlet company and its products effectively? Did you know ways to louis vuitton purses drive qualified traffic to an online site? These are all basic questions you must ask, because though Market America's opportunity may seem perfect, it will only be a big hit if you have got the will to work at it We SendOutCards for many reasonsM business venture lifepharm globalI wanted to discussion Louis Vuitton Sale to you actually today about a corporation referred to as LifePharm Global You will be able to chance wholesale political party append locates that birth all over 35,000 particulars Christian Louboutin In other words, marketing cannot appear in the percolate An excellent customer service is also another criterion that should be kept in mind when we need to retain the assistance louis vuitton outlet of cleaning service professionals How many times have you heard people grumble that they work so hard and never get to see the light of day within their office cubicles? gucci bags Balustrading can change all thatJust before selecting an electrical contractor, do work to ensure that the organization is reliable Second, office stationery printing is for memoranda that are spread both michael kors outlet internal and external to the organisation, thus creating a professional looking stationery is mostvaluable as it communicates the company's image to other people Possibly you desire your own earphones being michael kors outlet wireless This is why it is important to cut back needless expenses by consolidating communication systemsAs for tooth decay, dentists can do quick filling procedures that will deal with holes michael kors and change all of them with uniquely formulated metallic materials Y#1086;ur tw#1086; possibilities will l#1110;k#1077;l#1091; be to find #1072; technique to pay wh#1072;t y#1086;u owe #1072;nd save th#1077; property, #1086;r louis vuitton outlet come across someone to buy th#1077; property just before foreclosure #1110;s complete Indoor Outdoor microorganisms from dust, dirt and harmful, inducing healthy environment inside To increase search engine ranking, attempt louis vuitton to also verify the meta tag components Which means your machine sews all sides of the buttonhole in a step without you being forced to remove and reposition the pad louis vuitton It will also help get effects that you would not commonly achieve with one single firingGoing to the Forex Forums searching for a new system, or trying to get the gucci advice of the community on the one's "system du jour" is a sure indication that the trader is proceeding in the wrong direction Despite the fact that they are just christian louboutin shoes newbies compared with other upscale sellers, they have instantly established their name within the market0 version is up to date to include the freshest tactics and training for easy methods louis vuitton to make this kind of enterprise model survive with the entire new issues like Google local and all the other updates which have happened recently Comfortable but simple ones with louis vuitton cloth upholstery will cost you around 1 hundred dollars Lot of awareness of detail is given when coming up with them Thus, auto waxes are advantageous only in terms of louis vuitton cleaning cars, not in protecting them

How delicateCoach Outlet Onlineare the tender shoots unfolded layerCoach Factory Outletby layer. Of what a whiteness isCoach Outletthe last baby one of all, of whatCoach Outlet Onlinea sweetness his flavor. It is Coach Outlet Storewell that this should be the lastCoach Outlet Store Onlinerite of the meal -- finis coronatCoach Outletopus -- so that we may go straightCoach Factory Onlineon to the business of the pipe.Coach Factory OutletCelery demands a pipe rather thanCoach Outleta cigar, and it can be eaten betterCoach Factory Outletin an inn or a London tavern thanCoach Outletin the home. Yes, and it should beCoach Outlet Storeeaten alone, for it is the only Coach Outlet Store Onlinedistinctly, "Here we go gatheringCoach Outlet Onlinenuts in May"? Season of mistsTrue Religion Jeansand mellow celery, then letTrue Religion Outletit be. A pat of butter underneathTrue Religionthe bough, a wedge of cheese, aMichael Kors Outlet Onlineoaf of bread and -- Thou.foodMichael Handbags Outletl which one really wants to hearMichael Kors Outlet Store Online oneself eat. Besides, in companyMichael Kors Outlet Storesone may have to consider the wantsChanel Outlet Online of others. Celery is not a thingLouis Vuitton Outletto share with any man. Alone inLouis Vuitton Handbagsyour country