Skip to content

Commit 7871033

Browse files
committed
feat: port AJAX form submission from premium to core (#835)
- Migrates AJAX form submission from premium to core via REST API. - Replaces static 'Loading button text' with animated dots. - Merges AJAX JS logic directly into core forms.js bundle. - Includes backwards compatibility checks to prevent conflicts with older premium versions.
1 parent 2e465df commit 7871033

26 files changed

Lines changed: 346 additions & 897 deletions

CHANGELOG.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
11
Changelog
22
=========
33

4-
#### 4.12.2 - Apr 20, 2026
5-
6-
- Add Mailchimp Campaign Archive block and [mc4wp_campaigns] shortcode to show an archive Mailchimp email campaigns. Thanks to [Faisal Ahammad](https://faisalahammad.com/)!
7-
- WooCommerce: Allow "after email" position for sign-up checkbox when using Checkout Block.
8-
- WooCommerce: Detect use of Checkout Block and only show available positions.
9-
- Show warning when cron is behind schedule. Thanks to [Faisal Ahammad](https://faisalahammad.com/)!
10-
- Add preliminary support for [Mailchimp Site Tracking Pixel](https://mailchimp.com/help/about-mailchimp-site-tracking-pixel/). Thanks to [Faisal Ahammad](https://faisalahammad.com/)!
11-
12-
13-
#### 4.12.1 - Mar 26, 2026
14-
15-
- Supply a custom capability type to `register_post_type` for the `mc4wp-form` post type.
16-
- Limit allowed HTML in the various form messages to a safe subset on load (versus only on update).
17-
18-
194
#### 4.12.0 - Mar 9, 2026
205

216
- Remove the ability to unsubscribe through a form. You should migrate to the Mailchimp hosted form for this, which does email verification.
@@ -32,6 +17,7 @@ The following fixes and improvements were contributed by [Faisal Ahammad](https:
3217
- Enable live updates in Gravity Forms editor.
3318

3419

20+
3521
#### 4.11.0 - Jan 20, 2026
3622

3723
- Add form setting to remove tags from existing subscribers.

assets/src/js/campaigns-block.js

Lines changed: 0 additions & 77 deletions
This file was deleted.

assets/src/js/forms.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,123 @@ mc4wp.forms = forms
8585

8686
// expose mc4wp object globally
8787
window.mc4wp = mc4wp
88+
89+
// Initialize AJAX form submission if configured
90+
// The mc4wp_ajax_vars global is localized by PHP only when AJAX is enabled
91+
// and the premium AJAX module is not active.
92+
const ajaxConfig = window.mc4wp_ajax_vars
93+
if (typeof ajaxConfig !== 'undefined' && !ajaxConfig.inited) {
94+
const Loader = require('./forms/ajax-form-loader.js')
95+
let busy = false
96+
97+
/**
98+
* Handle AJAX response data and update the form accordingly.
99+
*
100+
* @param {object} form The mc4wp Form object
101+
* @param {object} response Parsed JSON response from REST API
102+
*/
103+
function handleResponseData (form, response) {
104+
forms.trigger('submitted', [form, null])
105+
106+
if (response.error) {
107+
form.setResponse(response.error.message)
108+
forms.trigger('error', [form, response.error.errors])
109+
} else if (response.code && response.message) {
110+
form.setResponse(`<div class="mc4wp-alert mc4wp-error"><p>${response.message}</p></div>`)
111+
forms.trigger('error', [form, [response.code]])
112+
} else {
113+
const data = form.getData()
114+
115+
forms.trigger('success', [form, data])
116+
forms.trigger(response.data.event, [form, data])
117+
118+
// for BC: always trigger "subscribed" event when firing "updated_subscriber" event
119+
if (response.data.event === 'updated_subscriber') {
120+
forms.trigger('subscribed', [form, data, true])
121+
}
122+
123+
if (response.data.hide_fields) {
124+
form.element.querySelector('.mc4wp-form-fields').style.display = 'none'
125+
}
126+
127+
form.setResponse(response.data.message)
128+
form.element.reset()
129+
130+
if (response.data.redirect_to) {
131+
window.location.href = response.data.redirect_to
132+
}
133+
}
134+
}
135+
136+
/**
137+
* Submits the given form over AJAX using the REST API endpoint.
138+
*
139+
* @param {object} form The mc4wp Form object
140+
*/
141+
function ajaxSubmit (form) {
142+
if (busy) {
143+
return
144+
}
145+
146+
const loader = new Loader(form.element, ajaxConfig.loading_character)
147+
loader.start()
148+
149+
form.setResponse('')
150+
busy = true
151+
152+
const request = new XMLHttpRequest()
153+
request.onreadystatechange = function () {
154+
if (request.readyState >= XMLHttpRequest.DONE) {
155+
loader.stop()
156+
busy = false
157+
158+
if (request.status >= 200 && request.status < 500) {
159+
try {
160+
const data = JSON.parse(request.responseText)
161+
handleResponseData(form, data)
162+
} catch (e) {
163+
// eslint-disable-next-line no-console
164+
console.error(`Mailchimp for WordPress: failed to parse response: "${e}"`)
165+
form.setResponse(`<div class="mc4wp-alert mc4wp-error"><p>${ajaxConfig.error_text}</p></div>`)
166+
}
167+
} else {
168+
// eslint-disable-next-line no-console
169+
console.error(`Mailchimp for WordPress: request error: "${request.responseText}"`)
170+
}
171+
}
172+
}
173+
request.open('POST', ajaxConfig.ajax_url, true)
174+
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
175+
request.setRequestHeader('Accept', 'application/json')
176+
request.send(form.getSerializedData())
177+
}
178+
179+
/**
180+
* Intercepts form submissions for AJAX-enabled forms.
181+
*
182+
* @param {object} form The mc4wp Form object
183+
* @param {Event} evt The original submit event
184+
*/
185+
function maybeSubmitOverAjax (form, evt) {
186+
if (form.element.getAttribute('class').indexOf('mc4wp-ajax') < 0) {
187+
return
188+
}
189+
190+
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
191+
document.activeElement.blur()
192+
}
193+
194+
try {
195+
ajaxSubmit(form)
196+
} catch (e) {
197+
// eslint-disable-next-line no-console
198+
console.error(e)
199+
return
200+
}
201+
202+
evt.preventDefault()
203+
}
204+
205+
forms.on('submit', maybeSubmitOverAjax)
206+
ajaxConfig.inited = true
207+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @param {HTMLInputElement} button
3+
* @returns {string}
4+
*/
5+
function getButtonText (button) {
6+
return button.innerHTML ? button.innerHTML : button.value
7+
}
8+
9+
/**
10+
* @param {HTMLInputElement} button
11+
* @param {string} text
12+
*/
13+
function setButtonText (button, text) {
14+
if (button.innerHTML) {
15+
button.innerHTML = text
16+
} else {
17+
button.value = text
18+
}
19+
}
20+
21+
/**
22+
* Constructs a new loader, which manipulates the form's button to show a loading indicator.
23+
*
24+
* @param {HTMLFormElement} formEl
25+
* @param {string} char
26+
* @constructor
27+
*/
28+
function Loader (formEl, char) {
29+
this.formEl = formEl
30+
this.button = formEl.querySelector('input[type="submit"], button[type="submit"]')
31+
this.char = char ?? '\u00B7'
32+
if (this.button) {
33+
this.originalButton = this.button.cloneNode(true)
34+
}
35+
}
36+
37+
/**
38+
* Starts the loading indicator
39+
*/
40+
Loader.prototype.start = function () {
41+
const { button, formEl, char } = this
42+
if (button) {
43+
const loadingText = this.button.getAttribute('data-loading-text')
44+
if (loadingText) {
45+
setButtonText(button, loadingText)
46+
} else {
47+
button.style.width = window.getComputedStyle(this.button).width
48+
setButtonText(button, char)
49+
this.loadingInterval = window.setInterval(this.tick.bind(this), 500)
50+
}
51+
} else {
52+
formEl.style.opacity = '0.5'
53+
}
54+
55+
formEl.className += ' mc4wp-loading'
56+
}
57+
58+
/**
59+
* Stops the loading indicator
60+
*/
61+
Loader.prototype.stop = function () {
62+
const { button, originalButton, formEl, loadingInterval } = this
63+
if (this.button) {
64+
button.style.width = originalButton.style.width
65+
const text = getButtonText(originalButton)
66+
setButtonText(button, text)
67+
window.clearInterval(loadingInterval)
68+
} else {
69+
formEl.style.opacity = ''
70+
}
71+
72+
formEl.className = formEl.className.replace('mc4wp-loading', '')
73+
}
74+
75+
/**
76+
* Represents a single step in the loading indicator
77+
*/
78+
Loader.prototype.tick = function () {
79+
const { button, char } = this
80+
const text = getButtonText(button)
81+
setButtonText(button, text.length >= 5 ? char : `${text} ${char}`)
82+
}
83+
84+
module.exports = Loader

autoload.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
'MC4WP_Admin' => '/includes/admin/class-admin.php',
1717
'MC4WP_Admin_Ads' => '/includes/admin/class-ads.php',
1818
'MC4WP_Admin_Ajax' => '/includes/admin/class-admin-ajax.php',
19-
'MC4WP_Admin_Cron_Notice' => '/includes/admin/class-cron-notice.php',
2019
'MC4WP_Admin_Messages' => '/includes/admin/class-admin-messages.php',
2120
'MC4WP_Admin_Review_Notice' => '/includes/admin/class-review-notice.php',
2221
'MC4WP_Admin_Texts' => '/includes/admin/class-admin-texts.php',
@@ -25,7 +24,6 @@
2524
'MC4WP_BuddyPress_Integration' => '/integrations/buddypress/class-buddypress.php',
2625
'MC4WP_Comment_Form_Integration' => '/integrations/wp-comment-form/class-comment-form.php',
2726
'MC4WP_Contact_Form_7_Integration' => '/integrations/contact-form-7/class-contact-form-7.php',
28-
'MC4WP_Campaign_Archive' => '/includes/campaigns/class-archive.php',
2927
'MC4WP_Container' => '/includes/class-container.php',
3028
'MC4WP_Custom_Integration' => '/integrations/custom/class-custom.php',
3129
'MC4WP_Debug_Log' => '/includes/class-debug-log.php',
@@ -73,7 +71,6 @@
7371
'MC4WP_Registration_Form_Integration' => '/integrations/wp-registration-form/class-registration-form.php',
7472
'MC4WP_Simple_Basic_Contact_Form_Integration' => '/integrations/simple-basic-contact-form/class-simple-basic-contact-form.php',
7573
'MC4WP_Tools' => '/includes/class-tools.php',
76-
'MC4WP_Tracking_Pixel' => '/includes/class-tracking-pixel.php',
7774
'MC4WP_Upgrade_Routines' => '/includes/admin/class-upgrade-routines.php',
7875
'MC4WP_User_Integration' => '/includes/integrations/class-user-integration.php',
7976
'MC4WP_WPForms_Field' => '/integrations/wpforms/class-field.php',

config/default-form-settings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
return [
4+
'ajax' => 1,
45
'css' => 0,
56
'double_optin' => 1,
67
'hide_after_success' => 0,

config/default-settings.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@
33
return [
44
'api_key' => '',
55
'debug_log_level' => 'warning',
6-
'email_on_error' => '',
7-
'tracking_pixel_id' => '',
6+
'email_on_error' => '',
87
];

includes/admin/class-admin.php

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ class MC4WP_Admin
3333
*/
3434
protected $review_notice;
3535

36-
/**
37-
* @var MC4WP_Admin_Cron_Notice
38-
*/
39-
protected $cron_notice;
40-
4136
/**
4237
* Constructor
4338
*
@@ -51,7 +46,6 @@ public function __construct(MC4WP_Admin_Tools $tools, MC4WP_Admin_Messages $mess
5146
$this->plugin_file = plugin_basename(MC4WP_PLUGIN_FILE);
5247
$this->ads = new MC4WP_Admin_Ads();
5348
$this->review_notice = new MC4WP_Admin_Review_Notice($tools);
54-
$this->cron_notice = new MC4WP_Admin_Cron_Notice($tools);
5549
}
5650

5751
/**
@@ -76,7 +70,6 @@ public function add_hooks()
7670
$this->ads->add_hooks();
7771
$this->messages->add_hooks();
7872
$this->review_notice->add_hooks();
79-
$this->cron_notice->add_hooks();
8073
}
8174

8275
/**
@@ -251,11 +244,6 @@ public function save_general_settings(array $settings)
251244
// Sanitize API key
252245
$settings['api_key'] = sanitize_text_field($settings['api_key']);
253246

254-
// Sanitize tracking pixel ID (alphanumeric and hyphens only)
255-
if (isset($settings['tracking_pixel_id'])) {
256-
$settings['tracking_pixel_id'] = preg_replace('/[^a-zA-Z0-9\-]/', '', $settings['tracking_pixel_id']);
257-
}
258-
259247
// if API key changed, empty Mailchimp cache
260248
if ($settings['api_key'] !== $current['api_key']) {
261249
delete_transient('mc4wp_mailchimp_lists');

0 commit comments

Comments
 (0)