Drupal 7 - Ajaxify a form in a popup

Public

src: http://www.jaypan.com/tutorial/drupal-7-ajaxify-form-popup
Step 1: Alias the Drupal core provided password reset form
Many times in Drupal 7, we want to re-use a form provided by core or by a contributed module, without affecting the original form itself. The password reset form exists in its natural state on the password reset page, located at /user/pass. In this tutorial, we will create our alias so that we can modify this form within our popup, while leaving it unaffected on the password reset page.

In order to achieve this goal, we need to create an 'alias' of the form. This means creating a new form ID that loads the password reset form. We can then make our alterations using the new form ID, which will then affect only our aliased form, and not the original.

To do this, in our module (pass_popup), we will implement hook_forms(). In this hook, we will tell Drupal that when we call a form with the aliased form ID pass_popup_user_pass, we want to show the user_pass form:

Get raw version
php
  1. function pass_popup_forms($form_id, $args)
  2. {
  3. $forms = array();
  4. // First we check if the form ID is equal to our alias
  5. if($form_id == 'pass_popup_user_pass')
  6. {
  7. // The user_pass() function is in the file user.pages.inc, which
  8. // is not included by default. As such, we need to manually include
  9. // it so our callback will work:
  10. module_load_include('inc', 'user', 'user.pages');
  11.  
  12. // Nest we tell Drupal to use the user_pass form for this form ID
  13. $forms['pass_popup_user_pass'] = array
  14. (
  15. 'callback' => 'user_pass',
  16. );
  17. }
  18.  
  19. return $forms;
  20. }

In the above code we have told the system that if we call drupal_get_form('pass_popup_user_pass') the system should show the form user_pass.
Step 2: Customize the form in our theme

The next thing we want to do is make some theme-specific alterations to the form. We will do this in our theme, pass_theme. In this case, we will remove the title of the field, and instead set it as the placeholder attribute of the field. This will put the text in the field itself as a default, and when the user starts typing, this default text will be removed. Note that this placeholder attribute only works in modern browsers, and will not work in IE9 or below.

In order to make this change, will will implement hook_form_FORM_ID_alter(), using our aliased form ID from step 1:

Get raw version
php
  1. function pass_theme_form_pass_popup_user_pass_alter(&$form, &$form_state)
  2. {
  3. // Create the placeholder from the field title
  4. $form['name']['#attributes']['placeholder'] = $form['name']['#title'];
  5.  
  6. // We want to ensure that user.pages.inc is included whenever
  7. // any step of the form happens, as this contains the validation and
  8. // submission functions for the user_pass() form:
  9. form_load_include($form_state, 'inc', 'user', 'user.pages');
  10. }

Step 3: Ajaxify the form so that it submits using Ajax

As we will be submitting the form within a popup, we don't want a page reload when we submit the form. Rather, we want to submit our form through AJAX, so that we can then hide the popup after the form has been submitted, keeping the user on the same page.

To do this, we will return to our module, and implement hook_form_FORM_ID_alter(), and ajaxify the submit button. We do this in the module, rather than the theme, as we will want the form to be ajaxified even if we switch themes in the future.

Get raw version
php
  1. function pass_popup_form_pass_popup_user_pass_alter(&$form, &$form_id)
  2. {
  3. // First, we will create a unique ID that can be used as a wrapper for the form. We could hard-code an
  4. // ID, however I prefer to use something that will be unique every time the form is called, on the off chance
  5. // that we someday want to use the same form multiple times on a single page. The #build_id of each form
  6. // is unique, and as such, we can use it to generate a unique form wrapper:
  7. $unique_key = 'form-wrapper-' . $form['#build_id'];
  8.  
  9. // Now we can add a wrapper to our entire form, that will be used as our ajax wrappre:
  10. $form['#prefix'] = '<div id="' . $unique_key . '">';
  11. $form['#suffix'] = '</div>';
  12.  
  13. // And finally we will ajaxify our submit button:
  14. $form['actions']['submit']['#ajax'] = array
  15. (
  16. // We pass the ID of our form wrapper as the ajax wrapper
  17. 'wrapper' => $unique_key,
  18. // We will get to this callback in the last step of the tutorial
  19. 'callback' => 'popup_pass_user_pass_ajax_submit',
  20. );
  21. }

Now our form will be submitted through AJAX when the submit button is clicked, rather than forcing a page reload. Note that we will look at the ajax callback function 'popup_pass_user_pass_ajax_submit() later on in the tutorial.

Step 4: Create a block that contains this aliased form

The next thing we want to do is create a block that contains this form. We will then set this block to be hidden upon page load, and insert it somewhere into the page, showing it when the 'forgot password' link is clicked.

To create the block, we will return to our module, and implement hook_block_info() to define the block, and hook_block_view() to provide the content of the block:

Get raw version
php
  1. function pass_popup_block_info()
  2. {
  3. // First, we define our block
  4. $blocks['custom_user_pass'] = array
  5. (
  6. // This title will show up on the block admin page
  7. 'info' => t('Password Reset Block'),
  8. );
  9.  
  10. return $blocks;
  11. }
  12.  
  13. function pass_popup_block_view($delta = '')
  14. {
  15. // Next, we create the content of our block
  16. if($delta == 'custom_user_pass')
  17. {
  18. $block = array
  19. (
  20. // The subject will be the title of the block as shown to the user
  21. 'subject' => t('Password Reset'),
  22. 'content' => array
  23. (
  24. // We use an arbitrary key here. This will be explained in the comments
  25. // after the code
  26. 'custom_user_pass_block' => array
  27. (
  28. // We use our aliased form ID to create a form, that become the contents
  29. // of the block.
  30. 'form' => drupal_get_form('pass_popup_user_pass'),
  31. ),
  32. ),
  33. );
  34.  
  35. return $block;
  36. }
  37. }

You'll notice that an arbitrary key was used in the block content, as a wrapper to the form. For more information on this, see issue #2 of our tutorial Custom Drupal Blocks the Right Way. As we wrapped our form with a prefix and a suffix in the last step when we ajaxified the form, we need this prefix and suffix to wrap the form, and not the entire block. As such, this arbitrary key is very necessary.

Step 5: Insert the block into the page upon page load so it's available to be shown in a popup

The next thing we want to do is insert the block into the page, so it can be shown as a popup when the 'forgot password' link is clicked. However, we don't want to have the block visible upon page load, so we need to set the block to be hidden when the page is loaded. To accomplish this, we will do two things. First, we will add some CSS to the block that hides the block when it's inserted into the page. At the same time, we will also add the CSS that will center the block on the screen when the block is shown to the user. We put the following CSS into the css file user_pass_block.css (to be attached later)

Get raw version
php
  1. #block-pass-popup-custom-user-pass
  2. {
  3. /* Hide the block */
  4. display:none;
  5.  
  6. /* Center it on the page for when it is shown */
  7. position: fixed;
  8. height: 250px;
  9. top: 50%;
  10. margin-top: -125px; /* half of height (negative) */
  11. width: 300px;
  12. left: 50%;
  13. margin-left: -150px; /* half of width (negative) */
  14.  
  15. /* Give it some styling to make it look nice */
  16. background-color:#FFF;
  17. border:solid black 3px;
  18. border-radius:5px;
  19. padding:10px;
  20. }
  21.  
  22. /* Fix the width of the text field */
  23. #block-pass-popup-custom-user-pass .form-text
  24. {
  25. width:100%;
  26. box-sizing:border-box;
  27. }

Now all we need to do is go to the admin block interface at /admin/structure/block, and drop the block that we named 'Password Reset Block' into the content region of the page. It will be hidden upon page load, so having it in the content region won't cause any issues. You should probably also go into the block settings, and set the block to only be shown to anonymous users, and only on pages that have a 'forgot password' link. If you are using the User Login block, it will have this link, so you can set the Password Reset block to show on the same pages as the User Login block.

Step 6: Create JavaScript to show the form when the 'forgot password' link is clicked

In this step, we are going to ajaxify any link that points to the page 'user/pass'. This way, if our javascript fails, or the user has javascript disabled, they will be directed to the password reset page. When javascript is enabled however, our block will be shown instead. To do this, we will use jQuery to target any link that has the href user/pass. We will create the JavaScript file user_pass_block.js (to be attached later):

Get raw version
javascript
  1. (function($, Drupal)
  2. {
  3. function showPassBlock()
  4. {
  5. $("#block-pass-popup-custom-user-pass").fadeIn(300);
  6. }
  7.  
  8. function hidePassBlock()
  9. {
  10. $("#block-pass-popup-custom-user-pass").fadeOut(300);
  11. }
  12.  
  13. function userPassLinkListener()
  14. {
  15. $("a[href*='user/pass']").once("user-pass-link-listener", function()
  16. {
  17. $(this).click(function(e)
  18. {
  19. e.preventDefault();
  20.  
  21. showPassBlock();
  22. });
  23. });
  24. }
  25.  
  26. Drupal.behaviors.userPassBlock = {
  27. attach:function()
  28. {
  29. userPassLinkListener();
  30. }
  31. };
  32.  
  33. // We need to create a custom AJAX command to be used by our AJAX submit
  34. // in order t hide our form after a successful submission. This will be triggered in
  35. // step 8 of the tutorial.
  36. Drupal.ajax.prototype.commands.passThemeHidePassPopup = function()
  37. {
  38. hidePassBlock();
  39. };
  40. }(jQuery, Drupal));

Step 7: Attach the JavaScript and CSS files to our block

In the previous two steps, we created the files user_pass_block.css and user_pass_block.js. We need to attach these to our block, so that they can be used. We will do this in our theme, as this functionality may change in the future if/when we change our theme. To do this, we will implement hook_block_view_MODULE_DELTA_alter():

Get raw version
php
  1. function pass_theme_block_view_pass_popup_custom_user_pass_alter(&$data, $block)
  2. {
  3. // We only want to attach the files if the block has been rendered on the page. So we check if $data is equal to TRUE.
  4. if($data)
  5. {
  6. // The block is to be shown on the page, so we will add our CSS and JS
  7.  
  8. // Get the path to our theme, as our CSS and JS file will be in the theme
  9. $path = drupal_get_path('theme', 'pass_theme');
  10. $data['content']['#attached']['css'][] = array
  11. (
  12. 'type' => 'file',
  13. 'data' => $path . '/user_pass_block.css',
  14. );
  15. $data['content']['#attached']['js'][] = array
  16. (
  17. 'type' => 'file',
  18. 'data' => $path . '/user_pass_block.js',
  19. );
  20. }
  21. }

Now, our CSS and JS will be included on the page any time our block is included on the page. This will ensure that the block is hidden, and that it is shown when links that point to the password reset page are clicked.

Step 8: Hide the form after a successful submission

The last step is to create our ajax callback function, popup_pass_user_pass_ajax_submit(), that we added to the submit button of the form back in step 3. This will be done in the module, not the theme, as we will want this functionality to persist in the event of a theme change. For more information on what we are doing in this callback, see our tutorial Calling a function after an AJAX event in Drupal 7.

Get raw version
php
  1. function popup_pass_user_pass_ajax_submit($form, &$form_state)
  2. {
  3. $commands = array();
  4.  
  5. // 1) Check if there were any errors in submitting the form. If there were not, we
  6. // will hide the block, as the user does not need to see it anymore. This will be done using
  7. // the custom command we defined in our JS file in step 6
  8.  
  9. // Get any messages. Make sure to pass FALSE as the second argument, as we want to be able
  10. // to render these messages later
  11. $messages = drupal_get_messages(NULL, FALSE);
  12. // We will execute our command if there are no error messages
  13. if(!count($messages) || !isset($messages['error']) || !count($messages['error']))
  14. {
  15. // There were no error messages, so we add the command to hide the block
  16. $commands[] = array
  17. (
  18. 'command' => 'passThemeHidePassPopup',
  19. );
  20. }
  21.  
  22.  
  23. // 2) Re-render the form, and replace the original form with the new re-rendered form.
  24. // In the event that there was a validation error, the re-rendered form will show the error fields,
  25. // and if there were no validation errors and the form was submitted, a fresh form will be shown
  26. // to the user. We also want to prepend any messages that were created, so the user can see
  27. // any success/failure messages
  28.  
  29. // The core-provided ajax_command_replace() is used, and NULL is passed as the selector, meaning
  30. // that the selector we defined in our #ajax will be used. The replacement value is our rendered form.
  31. $commands[] = ajax_command_replace(NULL, theme('status_messages') . render($form));
  32.  
  33. // 3) Return our ajax commands
  34. return array('#type' => 'ajax', '#commands' => $commands);
  35. }