Handling Form Submissions in WordPress with Admin-Post and Admin-Ajax

Handling Form Submissions in WordPress with Admin-Post and Admin-Ajax

WordPress provides incredible support for you to work with form submissions in your application. Whether you add a form in the admin or public facing areas, the built-in mechanism with the admin-post and admin-ajax scripts will allow you to handle your form requests efficiently.

In this article, I’ll show you how to handle custom form submissions using the WordPress API. I’ll walk you through the process of adding a custom form in the admin area of a plugin, handle the form submission via an HTML as well as an AJAX request, and write the form handler in PHP to validate, sanitize and process the form input.

While I’ll stay within the admin realms of WordPress, the same concepts are applicable while working with forms in the public facing areas.

I’ll also be making use of object-oriented programming constructs for the plugin; however, you can achieve the same result using procedural code as well. The practice plugin can be downloaded from here to follow along with the article.

Note: This article is intended for intermediate-advanced WordPress developers. It assumes that you have a working knowledge of HTML, JavaScript, jQuery, PHP and the WordPress Plugin API. If you’d like a refresher, I recommend that you read through the following:

Let’s get started by first understanding the built-in WordPress mechanism to handle a regular form post request.

Form Submissions with admin-post.php in WordPress

The gamut of hooks available in WordPress gives you great control over the flow of execution of your application. This is no different when it comes to processing forms. All you need is the correct hook to ‘hook into’ and add the custom form handler. The hooks for processing custom forms are dynamic in nature, meaning that the name of the hook partly depends on you.

To process submissions related to your form only, you need finer control as shown below:

WordPress form submission with admin-post.php
WordPress form submission with admin-post.php

This is done by pointing the form submission to the admin-post.php file located in the wp-admin directory of WordPress, and including a custom name for the action in the form. On doing so, WordPress will trigger two action hooks based on the logged in status of the user:

  • admin_post_{$action} for logged in users
  • admin_post_nopriv_{$action} for non-logged in users

Where $action is the name of the action that was passed through the form.

You can then use add_action to tie the PHP form handler to the triggered hooks, where you will have full control to process the form data with the $_GET and $_POST variables.

As you may have guessed already, despite its name, admin-post.php can handle POST and GET requests as well as requests for admin and non-admin areas of the application.

Let’s explore this with the help of a custom plugin.

The Object-Oriented Plugin Structure

My goal here is to help you understand everything that goes behind processing custom forms in WordPress with and without AJAX. For this article, I’ve prepared a custom plugin that you can download from here to follow along. I recommend that you have it open in a suitable editor and install it on a local WordPress setup only.

I built the plugin using object-oriented programming practices with the help of a plugin boilerplate. Boilerplate Starting Points are among the many best practices listed in the WordPress Plugin Handbook. They’re a great way to ensure consistency across your plugins, and save you a lot of time writing standard code. Over a period, you may even end up writing your own custom boilerplate based on your coding preferences. That’s what I did.

The plugin is based on my own plugin template which is a fork of the original WordPress Plugin Boilerplate project. It’s similar to the original project in many aspects but also has support for namespaces and autoloading. This way I don’t need to have unique prefixes for every class or function, and don’t end up with a lot of include and require statements. However, the minimum required PHP version for my plugin is 5.6.0.

Note: If you don’t use namespaces or use procedural code you must prefix everything.

Here’s how the plugin is structured in the backend:

  • inc/core/* – core functionality of the plugin
  • inc/admin/* – functionality related with the admin area
  • inc/frontend/* – functionality related with the public facing areas
  • inc/common/* – functionality shared between the admin and the frontend
oop-based-plugin structure
Plugin structure in the backend

The plugin has a top-level admin menu with two menu items for the form pages.

admin menu structure of the plugin
Admin menu structure of the plugin

To see how I added the admin menu pages, take a look at the define_admin_hooks() method in inc/core/class-init.php and the add_plugin_admin_menu() method in the inc/admin/class-admin.php of the plugin.

If you’d like to know more about adding admin pages to your plugin, have a look at our article about creating WordPress admin pages here.

Adding the Form to the Admin Page of the Plugin

When I added the “HTML Form Submit” menu page for the plugin, I had to also specify the callback to load the page content. This is where the form is added.

However, instead of directly writing the HTML in the html_form_page_content method, I used another file partials-html-form-view.phplocated in inc/admin/views for the form HTML and loaded it in the callback as shown below:

This is purely a coding preference. It allows me to keep my code readable by separating the HTML, and makes no difference to the output of the form on the plugin page.

plugin admin page with html form
HTML Form in the admin page of the plugin

Understanding Form Security, Structure, and Submission

The form that was added above has a select field with a drop-down list of existing WordPress users and two text fields for user input. However, this simple example has a lot going on behind the scenes. The form code below is self-explanatory, so let’s walk through the important elements:

Form Security

The most important thing to keep in mind when dealing with forms in the admin area of WordPress is security. Secure your form using a combination of both WordPress Nonces and current_user_can( $capability ). In my example, I’ve restricted entry to the form with if( current_user_can( 'edit_users' ) ), i.e. the form will be loaded only if the logged in user has the edit_users capability.

I also generated a custom nonce by using wp_create_nonce() and then added it as a hidden form field. You can instead use wp_nonce_field() to add it directly. Here’s a great article to understand Nonces in detail.

Form Structure

I’ve prefixed all form elements with the plugin name to ensure uniqueness. This is again a personal coding preference, as I can be sure of targeting only my form elements through JavaScript. I’ve also used the HTML5 required attribute to leave form validation to the browser.

plugin form inspect view in chrome
Inspecting the admin form

Form Submission

The form submission is made to the admin-post.php using the admin_url( 'admin-post.php' ) function rather than hardcoding the URL. When WordPress receives the form, it will look for the value of the action field to trigger the form hooks. In my case, it will generate the admin_post_nds_form_response hook. Had it been a page open to the public view, it would have triggered the admin_post_nopriv_nds_form_response hook.

The Form Handler for the POST request

At this stage, if you submit the form, you’ll be redirected to an empty page with the page URL set to the admin-post.php. This is because there is no form handler to process the request yet. To process the request, I registered my custom handler the_form_response in the define_admin_hooks() method of class-init.php like this: $this->loader->add_action( 'admin_post_nds_form_response', $plugin_admin, 'the_form_response');

If you were using procedural code you would simply do add_action( 'admin_post_nds_form_response', 'the_form_response');

the_form_response() is where I’ll have full access to the form data via the $_POST or $_GET superglobals. As shown below, I added a breakpoint to the callback in my IDE to be certain that the hook would work as expected.

pausing php script execution
Inspecting form input with XDebug

Form Validation and Input Sanitization

Before performing any operations, you must validate the nonce and sanitize the user input properly. I made use of the wp_verify_nonce( $nonce_name, $nonce_action ) function to verify the nonce, and sanitize_key() and sanitize_text_field() functions to sanitize the user input available in the $_POST variable. If the nonce verification fails, the user will get an error message as the server response, using the wp_die() WordPress function.

Note: I accessed the form data using the $_POST variable. Had I submitted the form using the get method, I would instead make use of the $_GET or $_REQUEST global variable.

Only when I’m sure that everything is in order, would I perform a WordPress operation like adding the user-meta to the selected user.

To know more about input sanitization, I recommend that you read through the WordPress Codex: Validating Sanitizing and Escaping User Data here.

Submitting the Server Response

After performing the server operations, it’s important to send the server response back to the user. To do this, you will first need to redirect the user back to an admin page or one that provides some feedback. I redirected the user back to the plugin page and used WordPress admin notices to display the server feedback. The server response in my example simply outputs the $_POST variable as a WordPress admin notice.

form post server response
Server response from the form handler

Progressive Enhancement

At this stage, I have a fully functional form in the admin area of my WordPress plugin. It’s secure and submits properly to my form handler, where the input data is sanitized and finally, the server response is visible. The form will work out of the box in all browsers that have support for HTML5. But there’s a lot I can do to improve the user experience such as adding AJAX support.

This approach of establishing a basic level of user experience that’s available in all browsers, and then adding advanced functionality for browsers that support it is called Progressive Enhancement.

Note: I’ve made the assumption that my users use modern browsers with HTML5 support. However, if the form had to be rendered on an older browser, the built-in HTML5 input validation for required fields would break. Can I Use is a great website that you can use to compare web features that are available across browsers and browser versions.

Form Submissions with AJAX (admin-ajax.php) in WordPress

AJAX in WordPress is handled via the wp-admin/admin-ajax.php file. Here’s an overview of how custom forms can be processed via AJAX in WordPress:

form support with ajax
Form submission with AJAX support in WordPress

You’ll notice that it’s quite similar to how forms are processed using admin-post.php. When WordPress receives an AJAX request it will create two hooks based on the supplied action:

  • wp_ajax_{$action} for logged in users
  • wp_ajax_nopriv_{$action} for non-logged in users

Where $action is the name of the action that was passed.

Adding AJAX Support to the Plugin Form

The second menu page of the plugin “Ajax Form Submit” loads the form that’s submitted via an AJAX request. It’s added to the menu page in the same manner as discussed earlier, and uses the partials-ajax-form-view.php file to load the form content. If you look at this file, you’ll notice that it’s nearly identical to the earlier form with the only differences being the value of the form id attribute and the title. Now that I can identify one form from the other, I can process just the second form via AJAX using JavaScript.

To add AJAX support, I performed the following steps:

  • Enqueued a JavaScript file to load the jQuery
  • Used jQuery submit event handler to prevent the normal form submission
  • Used jQuery.ajax() to submit the form to admin-ajax.php instead of admin-post.php

Note: If for some reason JavaScript is disabled in the browser, jQuery or AJAX will be unavailable too, but the form will still submit normally. This is because I left the form submission URL as admin-post.php in the form HTML.

Using JavaScript and jQuery to Post the Form

Here’s the JavaScript that I used to submit the form via AJAX.

event.preventDefault(); is what actually prevents the normal form submission.

I gathered the form data using jQuery’s serialize() function but there are many other ways to do this. One of them is using HTML5’s FormData interface. It’s beyond the scope of this article but it’s definitely worth looking at.

var ajax_form_data = $("#nds_add_user_meta_ajax_form").serialize();

I also added additional URL parameters to the serialized data, so I can distinguish between an AJAX and a regular request in the PHP form handler later.

ajax_form_data = ajax_form_data+'&ajaxrequest=true&submit=Submit+Form';

Typically, the X-Requested-With HTTP header is automatically set to XMLHttpRequest by the AJAX library. This can also be used to identify an AJAX request but it’s not always reliable.

The ajax() method of jQuery will submit the request to the server.

To get the form to submit to admin-ajax.php, I used an array params.ajaxurl that was passed in from PHP using wp_localize_script.

Note: The form data in my example includes the action that WordPress will use to generate the hooks for the AJAX request. The following hooks will be triggered by WordPress:

  • wp_ajax_nds_form_response for logged in users
  • wp_ajax_nopriv_nds_form_response for non-logged in users

The JavaScript file is enqueued in the enqueue_scripts() method of class-admin.php as below:

The ajaxurl Global Variable

You can also use a global JavaScript variable ajaxurl instead of passing the URL for admin-ajax.php from PHP. However, the variable is available only when dealing with the admin end and is unavailable when dealing with AJAX on the frontend.

Depending on the response from the server, the AJAX promise callbacks .done() and .fail() will execute accordingly. In my example, for a successful request, I’ve added the response to the empty div container #nds_form_feedback that was part of my form HTML. Finally, the fields are cleared by reseting the form.

The Form Handler for the AJAX Request

I’ve attached the same form handler the_form_response to the AJAX request as well.

And in the form handler, I used $_POST['ajaxrequest'] that was set manually in the JavaScript to distinguish between a normal and AJAX request.

pausing script execution to verify ajax
Validating the AJAX request using a breakpoint

That’s it. With AJAX, the response is displayed without the page being reloaded or redirected.

If JavaScript was disabled or did not load for some reason, $_POST['ajaxrequest'] would not be valid, and the form would submit normally by skipping the AJAX specific if( isset( $_POST['ajaxrequest'] ) && $_POST['ajaxrequest'] === 'true' ) code block.

You can certainly do a lot more to improve the user experience, and I recommend you read through the jQuery API documentation for AJAX here.

Additional Resources

We’ve covered a lot of ground here. AJAX is a fairly vast topic and is implemented in several ways. Here are some more examples of using AJAX in WordPress:

Karan Gupta
Do you use custom forms on your site? Let us know in the comments below.