Skip to content

Commit 401fb25

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 201d7f6 commit 401fb25

7 files changed

Lines changed: 293 additions & 8 deletions

File tree

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

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,

includes/forms/class-asset-manager.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ class MC4WP_Form_Asset_Manager
1818
*/
1919
private $load_typo_checker = false;
2020

21+
/**
22+
* @var bool Flag to determine whether AJAX form script should be enqueued.
23+
*/
24+
private $load_ajax = false;
25+
26+
/**
27+
* @var string Error text from first AJAX-enabled form, used as fallback in JavaScript.
28+
*/
29+
private $ajax_error_text = '';
30+
2131
/**
2232
* Add hooks
2333
*/
@@ -168,6 +178,12 @@ public function before_output_form($form)
168178
$this->print_dummy_javascript();
169179
$this->load_scripts = true;
170180

181+
// check if this form has AJAX enabled (skip if premium AJAX module is active)
182+
if (! empty($form->settings['ajax']) && ! $this->load_ajax && ! class_exists('MC4WP_AJAX_Forms')) {
183+
$this->load_ajax = true;
184+
$this->ajax_error_text = $form->get_message('error');
185+
}
186+
171187
// check if this form has typo checker enabled
172188
if (! empty($form->settings['email_typo_check'])) {
173189
$this->load_typo_checker = true;
@@ -230,6 +246,27 @@ public function load_scripts()
230246
wp_localize_script('mc4wp-forms-submitted', 'mc4wp_submitted_form', $submitted_form_data);
231247
}
232248

249+
// maybe load AJAX forms script
250+
if ($this->load_ajax) {
251+
// default loading character (bullet)
252+
$character = '&bull;';
253+
254+
/**
255+
* Filters the loading character used for AJAX form requests.
256+
*
257+
* @since 4.13.0
258+
*
259+
* @param string $character The loading character.
260+
*/
261+
$loading_character = (string) apply_filters('mc4wp_forms_ajax_loading_character', $character);
262+
263+
wp_localize_script('mc4wp-forms-api', 'mc4wp_ajax_vars', [
264+
'loading_character' => $loading_character,
265+
'ajax_url' => rest_url('mc4wp/v1/form'),
266+
'error_text' => $this->ajax_error_text,
267+
]);
268+
}
269+
233270
// print inline scripts
234271
echo '<script>';
235272
echo '(function() {';

includes/forms/class-form-element.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ protected function get_css_classes()
362362
$classes[] = 'mc4wp-form-' . $form->settings['css'];
363363
}
364364

365+
// add AJAX class if enabled
366+
if (! empty($form->settings['ajax'])) {
367+
$classes[] = 'mc4wp-ajax';
368+
}
369+
365370
// add classes from config array
366371
if (! empty($this->config['element_class'])) {
367372
$classes = array_merge($classes, explode(' ', $this->config['element_class']));

includes/forms/class-form-manager.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ public function register_endpoint()
141141
* Process requests to the form endpoint.
142142
*
143143
* A listener checks every request for a form submit, so we just need to fetch the listener and get its status.
144+
*
145+
* @return WP_REST_Response|WP_Error
144146
*/
145147
public function handle_endpoint()
146148
{
@@ -156,18 +158,30 @@ public function handle_endpoint()
156158
}
157159

158160
if ($form->has_errors()) {
159-
$message_key = $form->errors[0];
160-
$message = $form->get_message($message_key);
161-
return new WP_Error(
162-
$message_key,
163-
$message,
161+
return new WP_REST_Response(
164162
[
165-
'status' => 400,
166-
]
163+
'error' => [
164+
'type' => $form->errors[0],
165+
'message' => $form->get_response_html(),
166+
'errors' => $form->errors,
167+
],
168+
],
169+
400
167170
);
168171
}
169172

170-
return new WP_REST_Response(true, 200);
173+
$data = [
174+
'event' => $form->last_event,
175+
'message' => $form->get_response_html(),
176+
'hide_fields' => (bool) $form->settings['hide_after_success'],
177+
];
178+
179+
$redirect_url = $form->get_redirect_url();
180+
if (! empty($redirect_url)) {
181+
$data['redirect_to'] = $redirect_url;
182+
}
183+
184+
return new WP_REST_Response([ 'data' => $data ], 200);
171185
}
172186

173187
/**

includes/forms/views/tabs/form-settings.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,30 @@
175175
</td>
176176
</tr>
177177

178+
<?php
179+
// Only show AJAX settings if the premium AJAX module is not active, to avoid duplicate settings.
180+
if (! class_exists('MC4WP_AJAX_Forms')) {
181+
?>
182+
<tr valign="top">
183+
<th scope="row"><?php echo esc_html__('Enable AJAX form submission?', 'mailchimp-for-wp'); ?></th>
184+
<td class="nowrap">
185+
<label>
186+
<input type="radio" name="mc4wp_form[settings][ajax]" value="1" <?php checked($opts['ajax'], 1); ?> />&rlm;
187+
<?php echo esc_html__('Yes', 'mailchimp-for-wp'); ?>
188+
</label> &nbsp;
189+
<label>
190+
<input type="radio" name="mc4wp_form[settings][ajax]" value="0" <?php checked($opts['ajax'], 0); ?> />&rlm;
191+
<?php echo esc_html__('No', 'mailchimp-for-wp'); ?>
192+
</label>
193+
<p class="description">
194+
<?php echo esc_html__('Select "yes" if you want to use AJAX (JavaScript) to submit forms.', 'mailchimp-for-wp'); ?>
195+
</p>
196+
</td>
197+
</tr>
198+
<?php
199+
}
200+
?>
201+
178202
<?php do_action('mc4wp_admin_form_after_behaviour_settings_rows', $opts, $form); ?>
179203

180204
</table>

0 commit comments

Comments
 (0)