Drupal 7 - Ajaxify a form in a popup

Overview

In Drupal 7, it's often beneficial to put various forms into Ajax popups, to create a nice user experience. In this tutorial, we will take Drupal's core-provided password reset form, customize it, and put it in a popup so that when users click the 'forgot password' link, this popup pops up, and allows them to reset their password on the same page. After a sucessful submission, the popup will be hidden and the user will be back on the same page they were on.

This tutorial will show how to do the following:

  1. Alias the Drupal core provided password reset form
  2. Customize the form in our theme
  3. Ajaxify the form so that it submits using Ajax
  4. Create a block that contains this aliased form
  5. Insert the block into the page upon page load so it's available to be shown in a popup
  6. Create JavaScript to show the form when the 'forgot password' link is clicked
  7. Attach the JavaScript and CSS files to our block
  8. Hide the form after a successful submission

For the purposes of this tutorial, we will consider that the block and the form will be created in a custom module called pass_popup. This is because we will want to persist our block and our popup even if we change themes in the future. However, alterations to the form itself will be done in the theme, which we will call pass_theme. This is because these alterations will likely change if we change site themes in the future.

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:

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

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:

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

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.

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

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

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)

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

With this code, the block will be hidden, but when it is shown, it will be centered on the screen.

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):

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

  1. function pass_theme_block_view_pass_popup_custom_user_pass_alter(&$data, $block) {
  2. // 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.
  3. if ($data) {
  4. // The block is to be shown on the page, so we will add our CSS and JS
  5.  
  6. // Get the path to our theme, as our CSS and JS file will be in the theme
  7. $path = drupal_get_path('theme', 'pass_theme');
  8. $data['content']['#attached']['css'][] = [
  9. 'type' => 'file',
  10. 'data' => $path . '/user_pass_block.css',
  11. );
  12. $data['content']['#attached']['js'][] = [
  13. 'type' => 'file',
  14. 'data' => $path . '/user_pass_block.js',
  15. ];
  16. }
  17. }

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.

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

The above code will return our form, showing any messages to the user, and in the case that there were no errors, will hide the block after a successful submission.

Overview

In this tutorial, we created a customized password reset form, created a block to hold that form, inserted the block into the page on page load, showed it when a pass reset link is clicked, and hid it after a successful submission. Hopefully this can help you create nice form modal popups for your Drupal sites!