CTools Modal Windows - Part III
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
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_menu</span><span style="color: #007700">() {<br><br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'modal-test-callback/%ctools_js'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'page callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'mymodule_modal_callback'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">1</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'access arguments' </span><span style="color: #007700">=> array(</span><span style="color: #DD0000">'create article content'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">MENU_CALLBACK</span><span style="color: #007700">,<br> );<br><br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">; <br>}<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_block_info</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$blocks</span><span style="color: #007700">[</span><span style="color: #DD0000">'modal_test'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'info' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Modal Test Block'</span><span style="color: #007700">),<br> );<br> return </span><span style="color: #0000BB">$blocks</span><span style="color: #007700">;<br>}<br><br>function </span><span style="color: #0000BB">mymodule_block_view</span><span style="color: #007700">(</span><span style="color: #0000BB">$block_name</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$block_name </span><span style="color: #007700">== </span><span style="color: #DD0000">'modal_test'</span><span style="color: #007700">) {<br> <br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'modal'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'ajax'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">ctools_modal_add_js</span><span style="color: #007700">();<br> <br> return array(<br> </span><span style="color: #DD0000">'subject' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Modal Test Block Title!'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'content' </span><span style="color: #007700">=> </span><span style="color: #0000BB">ctools_modal_text_button</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Click Here!'</span><span style="color: #007700">),</span><span style="color: #DD0000">'modal-test-callback/nojs'</span><span style="color: #007700">,</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Click Here!'</span><span style="color: #007700">)),<br> );<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_modal_callback</span><span style="color: #007700">(</span><span style="color: #0000BB">$js </span><span style="color: #007700">= </span><span style="color: #0000BB">false</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// If the user doesn't have javascript, redirect them to the normal node/add/article page<br> </span><span style="color: #007700">if (!</span><span style="color: #0000BB">$js</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">drupal_goto</span><span style="color: #007700">(</span><span style="color: #DD0000">'node/add/article'</span><span style="color: #007700">);<br> }<br> else {<br> </span><span style="color: #FF8000">// Javascript is on, prepare ctools modals.<br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'ajax'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'modal'</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Pull in the global user, and prepare a blank node to pass to the node<br> // add form.<br> </span><span style="color: #007700">global </span><span style="color: #0000BB">$user</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$node </span><span style="color: #007700">= (object) array(</span><span style="color: #DD0000">'uid' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">uid</span><span style="color: #007700">, </span><span style="color: #DD0000">'name' </span><span style="color: #007700">=> (isset(</span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">name</span><span style="color: #007700">) ? </span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">name </span><span style="color: #007700">: </span><span style="color: #DD0000">''</span><span style="color: #007700">), </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'article'</span><span style="color: #007700">, </span><span style="color: #DD0000">'language' </span><span style="color: #007700">=> </span><span style="color: #0000BB">LANGUAGE_NONE</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$node</span><span style="color: #007700">-></span><span style="color: #0000BB">title </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">node_object_prepare</span><span style="color: #007700">(</span><span style="color: #0000BB">$node</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Add the node.pages.inc so that functions from the form can be used.<br> </span><span style="color: #0000BB">module_load_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'inc'</span><span style="color: #007700">, </span><span style="color: #DD0000">'node'</span><span style="color: #007700">, </span><span style="color: #DD0000">'node.pages'</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Prepare the form state, ctools reqruies ajax / title. The node add form<br> // requires node.<br> </span><span style="color: #0000BB">$form_state </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'ajax' </span><span style="color: #007700">=> </span><span style="color: #0000BB">true</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Add a new Article'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'node' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$node</span><span style="color: #007700">,<br> );<br><br> </span><span style="color: #FF8000">// Do the ctools_modal_form_wrapping of the node form. Returns a set of<br> // ajax commands in output.<br> </span><span style="color: #0000BB">$output </span><span style="color: #007700">= </span><span style="color: #0000BB">ctools_modal_form_wrapper</span><span style="color: #007700">(</span><span style="color: #DD0000">'article_node_form'</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">);<br><br> if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'executed'</span><span style="color: #007700">])) {<br><br> </span><span style="color: #FF8000">// Add the responder javascript, required by ctools<br> </span><span style="color: #0000BB">ctools_add_js</span><span style="color: #007700">(</span><span style="color: #DD0000">'ajax-responder'</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Create ajax command array, dismiss the modal window.<br> </span><span style="color: #0000BB">$output </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$output</span><span style="color: #007700">[] = </span><span style="color: #0000BB">ctools_modal_command_dismiss</span><span style="color: #007700">();<br> }<br><br> print </span><span style="color: #0000BB">ajax_render</span><span style="color: #007700">(</span><span style="color: #0000BB">$output</span><span style="color: #007700">);<br> exit;<br> }<br>} <br></span><span style="color: #0000BB">?></span></span>
Let's start with the top of the function.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_modal_callback</span><span style="color: #007700">(</span><span style="color: #0000BB">$js </span><span style="color: #007700">= </span><span style="color: #0000BB">false</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// If the user doesn't have javascript, redirect them to the normal node/add/article page<br> </span><span style="color: #007700">if (!</span><span style="color: #0000BB">$js</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">drupal_goto</span><span style="color: #007700">(</span><span style="color: #DD0000">'node/add/article'</span><span style="color: #007700">);<br> }<br> else {<br> ...<br> } <br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// Javascript is on, prepare ctools modals.<br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'ajax'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">ctools_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'modal'</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Pull in the global user, and prepare a blank node to pass to the node<br> // add form.<br> </span><span style="color: #007700">global </span><span style="color: #0000BB">$user</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$node </span><span style="color: #007700">= (object) array(</span><span style="color: #DD0000">'uid' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">uid</span><span style="color: #007700">, </span><span style="color: #DD0000">'name' </span><span style="color: #007700">=> (isset(</span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">name</span><span style="color: #007700">) ? </span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">name </span><span style="color: #007700">: </span><span style="color: #DD0000">''</span><span style="color: #007700">), </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'article'</span><span style="color: #007700">, </span><span style="color: #DD0000">'language' </span><span style="color: #007700">=> </span><span style="color: #0000BB">LANGUAGE_NONE</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$node</span><span style="color: #007700">-></span><span style="color: #0000BB">title </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">node_object_prepare</span><span style="color: #007700">(</span><span style="color: #0000BB">$node</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Add the node.pages.inc so that functions from the form can be used.<br> </span><span style="color: #0000BB">module_load_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'inc'</span><span style="color: #007700">, </span><span style="color: #DD0000">'node'</span><span style="color: #007700">, </span><span style="color: #DD0000">'node.pages'</span><span style="color: #007700">);<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// Prepare the form state, ctools reqruies ajax / title. The node add form<br> // requires node.<br> </span><span style="color: #0000BB">$form_state </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'ajax' </span><span style="color: #007700">=> </span><span style="color: #0000BB">true</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Add a new Person'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'node' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$node</span><span style="color: #007700">,<br> );<br><br> </span><span style="color: #FF8000">// Do the ctools_modal_form_wrapping of the node form. Returns a set of<br> // ajax commands in output.<br> </span><span style="color: #0000BB">$commands </span><span style="color: #007700">= </span><span style="color: #0000BB">ctools_modal_form_wrapper</span><span style="color: #007700">(</span><span style="color: #DD0000">'article_node_form'</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">);<br><br> if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'executed'</span><span style="color: #007700">])) {<br><br> </span><span style="color: #FF8000">// Add the responder javascript, required by ctools<br> </span><span style="color: #0000BB">ctools_add_js</span><span style="color: #007700">(</span><span style="color: #DD0000">'ajax-responder'</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Create ajax command array, dismiss the modal window.<br> </span><span style="color: #0000BB">$commands </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$commands </span><span style="color: #007700">= </span><span style="color: #0000BB">ctools_modal_command_dismiss</span><span style="color: #007700">();<br> }<br><br> print </span><span style="color: #0000BB">ajax_render</span><span style="color: #007700">(</span><span style="color: #0000BB">$commands</span><span style="color: #007700">);<br> exit;<br></span><span style="color: #0000BB">?></span></span>
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:
<span style="color: #000000"><span style="color: #0000BB"><?php<br> $output </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$output</span><span style="color: #007700">[] = </span><span style="color: #0000BB">ctools_modal_command_dismiss</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">$output</span><span style="color: #007700">[] = </span><span style="color: #0000BB">ajax_command_after</span><span style="color: #007700">(</span><span style="color: #DD0000">'.ctools-use-modal'</span><span style="color: #007700">,</span><span style="color: #0000BB">theme</span><span style="color: #007700">(</span><span style="color: #DD0000">'status_messages'</span><span style="color: #007700">));<br></span><span style="color: #0000BB">?></span></span>
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.