chunk-024-newsletter.js

I’ll build this as a production-style newsletter chunk for RX Theme: frontend form handling, validation, AJAX/REST submission, anti-spam fields, UI states, accessibility, local cooldown, and safe fallbacks.

Below is a strong advanced-level complete code for:

assets/static-js/static-chunks-js/chunk-024-newsletter.js

It is written as a standalone RX Theme newsletter module. It supports multiple newsletter forms, validation, loading state, success/error messages, honeypot spam protection, cooldown, local storage, REST/AJAX submit, fetch fallback, accessibility, and event hooks.

/*!
 * RX Theme - Newsletter Chunk
 * File: assets/static-js/static-chunks-js/chunk-024-newsletter.js
 * Version: 1.0.0
 *
 * Purpose:
 * Advanced newsletter subscription handling for RX Theme.
 *
 * Recommended form HTML:
 *
 * <form class="rx-newsletter-form" data-rx-newsletter-form>
 *   <input type="email" name="email" placeholder="Enter your email" required>
 *   <input type="text" name="name" placeholder="Your name">
 *   <input type="text" name="website" tabindex="-1" autocomplete="off" class="rx-hp-field">
 *   <button type="submit">Subscribe</button>
 *   <div class="rx-newsletter-message" data-rx-newsletter-message></div>
 * </form>
 */

(function () {
  'use strict';

  /**
   * ------------------------------------------------------------
   * 01. Prevent duplicate loading
   * ------------------------------------------------------------
   */
  if (window.RXNewsletterChunkLoaded) {
    return;
  }

  window.RXNewsletterChunkLoaded = true;

  /**
   * ------------------------------------------------------------
   * 02. Global configuration
   * ------------------------------------------------------------
   *
   * You can localize this from WordPress later using:
   *
   * wp_localize_script('rx-newsletter-chunk', 'RX_NEWSLETTER_CONFIG', [
   *   'ajaxUrl' => admin_url('admin-ajax.php'),
   *   'restUrl' => esc_url_raw(rest_url('rx/v1/newsletter')),
   *   'nonce'   => wp_create_nonce('wp_rest'),
   * ]);
   */

  var DEFAULT_CONFIG = {
    formSelector: '[data-rx-newsletter-form], .rx-newsletter-form',
    messageSelector: '[data-rx-newsletter-message], .rx-newsletter-message',

    emailSelector: 'input[type="email"], input[name="email"]',
    nameSelector: 'input[name="name"], input[name="full_name"], input[name="first_name"]',
    consentSelector: 'input[name="consent"], input[name="privacy"], input[type="checkbox"][required]',

    honeypotSelector: 'input[name="website"], input[name="url"], input[data-rx-honeypot]',

    submitSelector: 'button[type="submit"], input[type="submit"]',

    ajaxUrl: '',
    restUrl: '',
    nonce: '',

    action: 'rx_newsletter_subscribe',

    requestMethod: 'POST',
    requestTimeout: 15000,

    cooldownMinutes: 10,
    storagePrefix: 'rx_newsletter_',

    enableLocalStorage: true,
    enableCooldown: true,
    enableDuplicateCheck: true,
    enableAnalyticsEvents: true,
    enableConsoleDebug: false,

    minEmailLength: 6,
    maxEmailLength: 254,
    maxNameLength: 80,

    messages: {
      requiredEmail: 'Please enter your email address.',
      invalidEmail: 'Please enter a valid email address.',
      emailTooLong: 'Email address is too long.',
      nameTooLong: 'Name is too long.',
      consentRequired: 'Please accept the privacy policy before subscribing.',
      spamDetected: 'Subscription could not be processed.',
      alreadySubscribed: 'You are already subscribed from this browser.',
      cooldown: 'Please wait before trying again.',
      loading: 'Subscribing...',
      success: 'Thank you! Please check your email for confirmation.',
      error: 'Something went wrong. Please try again.',
      networkError: 'Network error. Please check your connection and try again.',
      timeout: 'Request timed out. Please try again.',
      missingEndpoint: 'Newsletter endpoint is not configured.'
    },

    classNames: {
      loading: 'is-loading',
      success: 'is-success',
      error: 'is-error',
      disabled: 'is-disabled',
      validated: 'is-validated'
    }
  };

  var RXNewsletterConfig = mergeObjects(
    DEFAULT_CONFIG,
    window.RX_NEWSLETTER_CONFIG || window.rxNewsletterConfig || {}
  );

  /**
   * ------------------------------------------------------------
   * 03. Utility functions
   * ------------------------------------------------------------
   */

  function log() {
    if (!RXNewsletterConfig.enableConsoleDebug) {
      return;
    }

    if (window.console && typeof window.console.log === 'function') {
      window.console.log.apply(window.console, ['[RX Newsletter]'].concat(Array.prototype.slice.call(arguments)));
    }
  }

  function mergeObjects(target, source) {
    var output = {};
    var key;

    for (key in target) {
      if (Object.prototype.hasOwnProperty.call(target, key)) {
        if (isPlainObject(target[key])) {
          output[key] = mergeObjects(target[key], {});
        } else {
          output[key] = target[key];
        }
      }
    }

    for (key in source) {
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        if (isPlainObject(source[key]) && isPlainObject(output[key])) {
          output[key] = mergeObjects(output[key], source[key]);
        } else {
          output[key] = source[key];
        }
      }
    }

    return output;
  }

  function isPlainObject(value) {
    return Object.prototype.toString.call(value) === '[object Object]';
  }

  function trim(value) {
    return String(value || '').replace(/^\s+|\s+$/g, '');
  }

  function normalizeEmail(email) {
    return trim(email).toLowerCase();
  }

  function safeQuery(parent, selector) {
    if (!parent || !selector) {
      return null;
    }

    try {
      return parent.querySelector(selector);
    } catch (error) {
      log('Invalid selector:', selector, error);
      return null;
    }
  }

  function safeQueryAll(parent, selector) {
    if (!parent || !selector) {
      return [];
    }

    try {
      return Array.prototype.slice.call(parent.querySelectorAll(selector));
    } catch (error) {
      log('Invalid selector:', selector, error);
      return [];
    }
  }

  function hasClass(element, className) {
    if (!element || !className) {
      return false;
    }

    return element.classList.contains(className);
  }

  function addClass(element, className) {
    if (!element || !className) {
      return;
    }

    element.classList.add(className);
  }

  function removeClass(element, className) {
    if (!element || !className) {
      return;
    }

    element.classList.remove(className);
  }

  function removeClasses(element, classNames) {
    if (!element || !classNames) {
      return;
    }

    Object.keys(classNames).forEach(function (key) {
      removeClass(element, classNames[key]);
    });
  }

  function setAttr(element, attr, value) {
    if (!element || !attr) {
      return;
    }

    element.setAttribute(attr, value);
  }

  function removeAttr(element, attr) {
    if (!element || !attr) {
      return;
    }

    element.removeAttribute(attr);
  }

  function dispatchRXEvent(name, detail) {
    if (!RXNewsletterConfig.enableAnalyticsEvents) {
      return;
    }

    try {
      var event = new CustomEvent(name, {
        detail: detail || {},
        bubbles: true,
        cancelable: false
      });

      document.dispatchEvent(event);
    } catch (error) {
      log('Event dispatch failed:', name, error);
    }
  }

  function now() {
    return new Date().getTime();
  }

  function supportsLocalStorage() {
    if (!RXNewsletterConfig.enableLocalStorage) {
      return false;
    }

    try {
      var testKey = RXNewsletterConfig.storagePrefix + 'test';
      window.localStorage.setItem(testKey, '1');
      window.localStorage.removeItem(testKey);
      return true;
    } catch (error) {
      return false;
    }
  }

  var canUseLocalStorage = supportsLocalStorage();

  function storageGet(key) {
    if (!canUseLocalStorage) {
      return null;
    }

    try {
      return window.localStorage.getItem(RXNewsletterConfig.storagePrefix + key);
    } catch (error) {
      return null;
    }
  }

  function storageSet(key, value) {
    if (!canUseLocalStorage) {
      return;
    }

    try {
      window.localStorage.setItem(RXNewsletterConfig.storagePrefix + key, String(value));
    } catch (error) {
      log('Local storage set failed:', error);
    }
  }

  function storageRemove(key) {
    if (!canUseLocalStorage) {
      return;
    }

    try {
      window.localStorage.removeItem(RXNewsletterConfig.storagePrefix + key);
    } catch (error) {
      log('Local storage remove failed:', error);
    }
  }

  function hashEmail(email) {
    var hash = 0;
    var i;
    var chr;

    if (!email || email.length === 0) {
      return '0';
    }

    for (i = 0; i < email.length; i++) {
      chr = email.charCodeAt(i);
      hash = ((hash << 5) - hash) + chr;
      hash |= 0;
    }

    return String(Math.abs(hash));
  }

  /**
   * ------------------------------------------------------------
   * 04. Validation functions
   * ------------------------------------------------------------
   */

  function isValidEmail(email) {
    var value = normalizeEmail(email);

    if (!value) {
      return false;
    }

    if (value.length < RXNewsletterConfig.minEmailLength) {
      return false;
    }

    if (value.length > RXNewsletterConfig.maxEmailLength) {
      return false;
    }

    /*
     * Practical email regex.
     * Not too strict, not too loose.
     */
    return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value);
  }

  function validateNewsletterForm(form) {
    var emailField = safeQuery(form, RXNewsletterConfig.emailSelector);
    var nameField = safeQuery(form, RXNewsletterConfig.nameSelector);
    var consentField = safeQuery(form, RXNewsletterConfig.consentSelector);
    var honeypotField = safeQuery(form, RXNewsletterConfig.honeypotSelector);

    var email = emailField ? normalizeEmail(emailField.value) : '';
    var name = nameField ? trim(nameField.value) : '';

    if (honeypotField && trim(honeypotField.value) !== '') {
      return {
        valid: false,
        type: 'spam',
        message: RXNewsletterConfig.messages.spamDetected
      };
    }

    if (!email) {
      return {
        valid: false,
        type: 'required_email',
        field: emailField,
        message: RXNewsletterConfig.messages.requiredEmail
      };
    }

    if (email.length > RXNewsletterConfig.maxEmailLength) {
      return {
        valid: false,
        type: 'email_too_long',
        field: emailField,
        message: RXNewsletterConfig.messages.emailTooLong
      };
    }

    if (!isValidEmail(email)) {
      return {
        valid: false,
        type: 'invalid_email',
        field: emailField,
        message: RXNewsletterConfig.messages.invalidEmail
      };
    }

    if (name && name.length > RXNewsletterConfig.maxNameLength) {
      return {
        valid: false,
        type: 'name_too_long',
        field: nameField,
        message: RXNewsletterConfig.messages.nameTooLong
      };
    }

    if (consentField && consentField.required && !consentField.checked) {
      return {
        valid: false,
        type: 'consent_required',
        field: consentField,
        message: RXNewsletterConfig.messages.consentRequired
      };
    }

    return {
      valid: true,
      email: email,
      name: name
    };
  }

  /**
   * ------------------------------------------------------------
   * 05. UI functions
   * ------------------------------------------------------------
   */

  function getMessageElement(form) {
    var messageElement = safeQuery(form, RXNewsletterConfig.messageSelector);

    if (!messageElement) {
      messageElement = document.createElement('div');
      messageElement.className = 'rx-newsletter-message';
      messageElement.setAttribute('data-rx-newsletter-message', '');
      messageElement.setAttribute('aria-live', 'polite');
      form.appendChild(messageElement);
    }

    return messageElement;
  }

  function setMessage(form, type, message) {
    var messageElement = getMessageElement(form);

    removeClass(messageElement, RXNewsletterConfig.classNames.success);
    removeClass(messageElement, RXNewsletterConfig.classNames.error);
    removeClass(messageElement, RXNewsletterConfig.classNames.loading);

    if (type === 'success') {
      addClass(messageElement, RXNewsletterConfig.classNames.success);
    }

    if (type === 'error') {
      addClass(messageElement, RXNewsletterConfig.classNames.error);
    }

    if (type === 'loading') {
      addClass(messageElement, RXNewsletterConfig.classNames.loading);
    }

    messageElement.textContent = message || '';
    setAttr(messageElement, 'role', type === 'error' ? 'alert' : 'status');
    setAttr(messageElement, 'aria-live', type === 'error' ? 'assertive' : 'polite');
  }

  function clearMessage(form) {
    var messageElement = getMessageElement(form);
    messageElement.textContent = '';
    removeClass(messageElement, RXNewsletterConfig.classNames.success);
    removeClass(messageElement, RXNewsletterConfig.classNames.error);
    removeClass(messageElement, RXNewsletterConfig.classNames.loading);
  }

  function setLoading(form, isLoading) {
    var submitButton = safeQuery(form, RXNewsletterConfig.submitSelector);
    var fields = safeQueryAll(form, 'input, button, select, textarea');

    if (isLoading) {
      addClass(form, RXNewsletterConfig.classNames.loading);
      setAttr(form, 'aria-busy', 'true');

      fields.forEach(function (field) {
        if (field.type !== 'hidden') {
          setAttr(field, 'data-rx-was-disabled', field.disabled ? '1' : '0');
          field.disabled = true;
        }
      });

      if (submitButton) {
        if (!submitButton.getAttribute('data-rx-original-text')) {
          submitButton.setAttribute('data-rx-original-text', submitButton.textContent || submitButton.value || '');
        }

        if (submitButton.tagName.toLowerCase() === 'input') {
          submitButton.value = RXNewsletterConfig.messages.loading;
        } else {
          submitButton.textContent = RXNewsletterConfig.messages.loading;
        }
      }
    } else {
      removeClass(form, RXNewsletterConfig.classNames.loading);
      removeAttr(form, 'aria-busy');

      fields.forEach(function (field) {
        var wasDisabled = field.getAttribute('data-rx-was-disabled');

        if (wasDisabled === '0') {
          field.disabled = false;
        }

        removeAttr(field, 'data-rx-was-disabled');
      });

      if (submitButton) {
        var originalText = submitButton.getAttribute('data-rx-original-text');

        if (originalText) {
          if (submitButton.tagName.toLowerCase() === 'input') {
            submitButton.value = originalText;
          } else {
            submitButton.textContent = originalText;
          }
        }
      }
    }
  }

  function markFieldInvalid(field, message) {
    if (!field) {
      return;
    }

    setAttr(field, 'aria-invalid', 'true');

    try {
      field.focus({ preventScroll: false });
    } catch (error) {
      field.focus();
    }

    if (field.setCustomValidity) {
      field.setCustomValidity(message || '');
    }
  }

  function clearFieldInvalid(field) {
    if (!field) {
      return;
    }

    setAttr(field, 'aria-invalid', 'false');

    if (field.setCustomValidity) {
      field.setCustomValidity('');
    }
  }

  function clearAllInvalidStates(form) {
    var fields = safeQueryAll(form, 'input, select, textarea');

    fields.forEach(function (field) {
      clearFieldInvalid(field);
    });
  }

  /**
   * ------------------------------------------------------------
   * 06. Cooldown and duplicate prevention
   * ------------------------------------------------------------
   */

  function isCooldownActive() {
    if (!RXNewsletterConfig.enableCooldown) {
      return false;
    }

    var lastSubmit = parseInt(storageGet('last_submit_time'), 10);

    if (!lastSubmit) {
      return false;
    }

    var cooldownMs = RXNewsletterConfig.cooldownMinutes * 60 * 1000;
    return now() - lastSubmit < cooldownMs;
  }

  function setCooldown() {
    if (!RXNewsletterConfig.enableCooldown) {
      return;
    }

    storageSet('last_submit_time', now());
  }

  function hasSubscribedEmail(email) {
    if (!RXNewsletterConfig.enableDuplicateCheck) {
      return false;
    }

    var emailHash = hashEmail(normalizeEmail(email));
    return storageGet('subscribed_' + emailHash) === '1';
  }

  function rememberSubscribedEmail(email) {
    if (!RXNewsletterConfig.enableDuplicateCheck) {
      return;
    }

    var emailHash = hashEmail(normalizeEmail(email));
    storageSet('subscribed_' + emailHash, '1');
  }

  /**
   * ------------------------------------------------------------
   * 07. Data collection
   * ------------------------------------------------------------
   */

  function collectFormData(form, validation) {
    var email = validation.email || '';
    var name = validation.name || '';

    var data = {
      action: RXNewsletterConfig.action,
      email: email,
      name: name,
      source: form.getAttribute('data-source') || form.getAttribute('data-rx-source') || 'rx-theme',
      page_url: window.location.href,
      page_title: document.title || '',
      referrer: document.referrer || '',
      timestamp: new Date().toISOString()
    };

    var campaign = form.getAttribute('data-campaign') || form.getAttribute('data-rx-campaign');
    var category = form.getAttribute('data-category') || form.getAttribute('data-rx-category');
    var tag = form.getAttribute('data-tag') || form.getAttribute('data-rx-tag');

    if (campaign) {
      data.campaign = campaign;
    }

    if (category) {
      data.category = category;
    }

    if (tag) {
      data.tag = tag;
    }

    var customFields = safeQueryAll(form, '[data-rx-field]');

    customFields.forEach(function (field) {
      var key = field.getAttribute('data-rx-field');

      if (!key) {
        return;
      }

      if (field.type === 'checkbox') {
        data[key] = field.checked ? '1' : '0';
      } else {
        data[key] = trim(field.value);
      }
    });

    return data;
  }

  function objectToFormData(object) {
    var formData = new FormData();

    Object.keys(object).forEach(function (key) {
      if (object[key] !== undefined && object[key] !== null) {
        formData.append(key, object[key]);
      }
    });

    if (RXNewsletterConfig.nonce) {
      formData.append('nonce', RXNewsletterConfig.nonce);
      formData.append('_wpnonce', RXNewsletterConfig.nonce);
    }

    return formData;
  }

  function objectToUrlEncoded(object) {
    var pairs = [];

    Object.keys(object).forEach(function (key) {
      if (object[key] !== undefined && object[key] !== null) {
        pairs.push(
          encodeURIComponent(key) + '=' + encodeURIComponent(object[key])
        );
      }
    });

    if (RXNewsletterConfig.nonce) {
      pairs.push('nonce=' + encodeURIComponent(RXNewsletterConfig.nonce));
      pairs.push('_wpnonce=' + encodeURIComponent(RXNewsletterConfig.nonce));
    }

    return pairs.join('&');
  }

  /**
   * ------------------------------------------------------------
   * 08. Network request
   * ------------------------------------------------------------
   */

  function getEndpoint() {
    if (RXNewsletterConfig.restUrl) {
      return {
        url: RXNewsletterConfig.restUrl,
        type: 'rest'
      };
    }

    if (RXNewsletterConfig.ajaxUrl) {
      return {
        url: RXNewsletterConfig.ajaxUrl,
        type: 'ajax'
      };
    }

    if (window.ajaxurl) {
      return {
        url: window.ajaxurl,
        type: 'ajax'
      };
    }

    return {
      url: '',
      type: ''
    };
  }

  function submitNewsletter(data) {
    var endpoint = getEndpoint();

    if (!endpoint.url) {
      return Promise.reject({
        type: 'missing_endpoint',
        message: RXNewsletterConfig.messages.missingEndpoint
      });
    }

    if (!window.fetch) {
      return submitNewsletterWithXHR(endpoint, data);
    }

    return submitNewsletterWithFetch(endpoint, data);
  }

  function submitNewsletterWithFetch(endpoint, data) {
    var controller = null;
    var timeoutId = null;
    var headers = {};

    if (window.AbortController) {
      controller = new AbortController();
      timeoutId = window.setTimeout(function () {
        controller.abort();
      }, RXNewsletterConfig.requestTimeout);
    }

    if (endpoint.type === 'rest' && RXNewsletterConfig.nonce) {
      headers['X-WP-Nonce'] = RXNewsletterConfig.nonce;
    }

    var body;

    if (endpoint.type === 'rest') {
      headers['Content-Type'] = 'application/json';
      body = JSON.stringify(data);
    } else {
      headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
      body = objectToUrlEncoded(data);
    }

    return window.fetch(endpoint.url, {
      method: RXNewsletterConfig.requestMethod,
      headers: headers,
      body: body,
      credentials: 'same-origin',
      signal: controller ? controller.signal : undefined
    })
      .then(function (response) {
        if (timeoutId) {
          window.clearTimeout(timeoutId);
        }

        return response.text().then(function (text) {
          var json = null;

          try {
            json = text ? JSON.parse(text) : {};
          } catch (error) {
            json = {
              success: response.ok,
              message: text
            };
          }

          if (!response.ok) {
            throw {
              type: 'http_error',
              status: response.status,
              response: json,
              message: getResponseMessage(json) || RXNewsletterConfig.messages.error
            };
          }

          return normalizeResponse(json);
        });
      })
      .catch(function (error) {
        if (timeoutId) {
          window.clearTimeout(timeoutId);
        }

        if (error && error.name === 'AbortError') {
          throw {
            type: 'timeout',
            message: RXNewsletterConfig.messages.timeout
          };
        }

        throw error;
      });
  }

  function submitNewsletterWithXHR(endpoint, data) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();
      var timeoutFired = false;

      xhr.open(RXNewsletterConfig.requestMethod, endpoint.url, true);
      xhr.timeout = RXNewsletterConfig.requestTimeout;

      if (endpoint.type === 'rest') {
        xhr.setRequestHeader('Content-Type', 'application/json');

        if (RXNewsletterConfig.nonce) {
          xhr.setRequestHeader('X-WP-Nonce', RXNewsletterConfig.nonce);
        }
      } else {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
      }

      xhr.onreadystatechange = function () {
        if (xhr.readyState !== 4 || timeoutFired) {
          return;
        }

        var json = null;

        try {
          json = xhr.responseText ? JSON.parse(xhr.responseText) : {};
        } catch (error) {
          json = {
            success: xhr.status >= 200 && xhr.status < 300,
            message: xhr.responseText
          };
        }

        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(normalizeResponse(json));
        } else {
          reject({
            type: 'http_error',
            status: xhr.status,
            response: json,
            message: getResponseMessage(json) || RXNewsletterConfig.messages.error
          });
        }
      };

      xhr.onerror = function () {
        reject({
          type: 'network_error',
          message: RXNewsletterConfig.messages.networkError
        });
      };

      xhr.ontimeout = function () {
        timeoutFired = true;
        reject({
          type: 'timeout',
          message: RXNewsletterConfig.messages.timeout
        });
      };

      if (endpoint.type === 'rest') {
        xhr.send(JSON.stringify(data));
      } else {
        xhr.send(objectToUrlEncoded(data));
      }
    });
  }

  function normalizeResponse(response) {
    var normalized = response || {};

    /*
     * Supports:
     * WordPress AJAX: { success: true, data: { message: "..." } }
     * REST: { success: true, message: "..." }
     */

    if (normalized.success === true) {
      return {
        success: true,
        message: getResponseMessage(normalized) || RXNewsletterConfig.messages.success,
        raw: normalized
      };
    }

    if (normalized.status === 'success') {
      return {
        success: true,
        message: getResponseMessage(normalized) || RXNewsletterConfig.messages.success,
        raw: normalized
      };
    }

    if (normalized.subscribed === true) {
      return {
        success: true,
        message: getResponseMessage(normalized) || RXNewsletterConfig.messages.success,
        raw: normalized
      };
    }

    return {
      success: false,
      message: getResponseMessage(normalized) || RXNewsletterConfig.messages.error,
      raw: normalized
    };
  }

  function getResponseMessage(response) {
    if (!response) {
      return '';
    }

    if (typeof response.message === 'string') {
      return response.message;
    }

    if (response.data && typeof response.data.message === 'string') {
      return response.data.message;
    }

    if (response.error && typeof response.error.message === 'string') {
      return response.error.message;
    }

    if (typeof response.data === 'string') {
      return response.data;
    }

    return '';
  }

  /**
   * ------------------------------------------------------------
   * 09. Main submit handler
   * ------------------------------------------------------------
   */

  function handleNewsletterSubmit(event) {
    var form = event.currentTarget;

    if (!form || hasClass(form, RXNewsletterConfig.classNames.loading)) {
      return;
    }

    event.preventDefault();

    clearMessage(form);
    clearAllInvalidStates(form);

    var validation = validateNewsletterForm(form);

    if (!validation.valid) {
      if (validation.field) {
        markFieldInvalid(validation.field, validation.message);
      }

      setMessage(form, 'error', validation.message);

      dispatchRXEvent('rx:newsletter:validation_error', {
        form: form,
        type: validation.type,
        message: validation.message
      });

      return;
    }

    if (isCooldownActive()) {
      setMessage(form, 'error', RXNewsletterConfig.messages.cooldown);

      dispatchRXEvent('rx:newsletter:cooldown', {
        form: form
      });

      return;
    }

    if (hasSubscribedEmail(validation.email)) {
      setMessage(form, 'success', RXNewsletterConfig.messages.alreadySubscribed);

      dispatchRXEvent('rx:newsletter:already_subscribed', {
        form: form,
        email: validation.email
      });

      return;
    }

    var data = collectFormData(form, validation);

    setLoading(form, true);
    setMessage(form, 'loading', RXNewsletterConfig.messages.loading);
    setCooldown();

    dispatchRXEvent('rx:newsletter:before_submit', {
      form: form,
      data: data
    });

    submitNewsletter(data)
      .then(function (result) {
        setLoading(form, false);

        if (result.success) {
          rememberSubscribedEmail(validation.email);

          addClass(form, RXNewsletterConfig.classNames.success);
          removeClass(form, RXNewsletterConfig.classNames.error);

          setMessage(form, 'success', result.message || RXNewsletterConfig.messages.success);

          try {
            form.reset();
          } catch (error) {
            log('Form reset failed:', error);
          }

          dispatchRXEvent('rx:newsletter:success', {
            form: form,
            email: validation.email,
            response: result.raw
          });

          return;
        }

        addClass(form, RXNewsletterConfig.classNames.error);
        removeClass(form, RXNewsletterConfig.classNames.success);

        setMessage(form, 'error', result.message || RXNewsletterConfig.messages.error);

        dispatchRXEvent('rx:newsletter:error', {
          form: form,
          email: validation.email,
          response: result.raw
        });
      })
      .catch(function (error) {
        setLoading(form, false);

        addClass(form, RXNewsletterConfig.classNames.error);
        removeClass(form, RXNewsletterConfig.classNames.success);

        var message = error && error.message
          ? error.message
          : RXNewsletterConfig.messages.networkError;

        setMessage(form, 'error', message);

        dispatchRXEvent('rx:newsletter:error', {
          form: form,
          email: validation.email,
          error: error
        });

        log('Submit failed:', error);
      });
  }

  /**
   * ------------------------------------------------------------
   * 10. Live field improvement
   * ------------------------------------------------------------
   */

  function handleFieldInput(event) {
    var field = event.target;

    if (!field) {
      return;
    }

    if (field.getAttribute('aria-invalid') === 'true') {
      clearFieldInvalid(field);
    }

    var form = closestForm(field);

    if (form) {
      removeClass(form, RXNewsletterConfig.classNames.error);
    }
  }

  function closestForm(element) {
    if (!element) {
      return null;
    }

    if (element.closest) {
      return element.closest(RXNewsletterConfig.formSelector);
    }

    while (element && element !== document) {
      if (matchesSelector(element, RXNewsletterConfig.formSelector)) {
        return element;
      }

      element = element.parentNode;
    }

    return null;
  }

  function matchesSelector(element, selector) {
    if (!element || element.nodeType !== 1) {
      return false;
    }

    var proto = Element.prototype;
    var fn = proto.matches ||
      proto.matchesSelector ||
      proto.webkitMatchesSelector ||
      proto.mozMatchesSelector ||
      proto.msMatchesSelector ||
      proto.oMatchesSelector;

    if (!fn) {
      return false;
    }

    try {
      return fn.call(element, selector);
    } catch (error) {
      return false;
    }
  }

  /**
   * ------------------------------------------------------------
   * 11. Honeypot enhancement
   * ------------------------------------------------------------
   */

  function prepareHoneypot(form) {
    var honeypotField = safeQuery(form, RXNewsletterConfig.honeypotSelector);

    if (!honeypotField) {
      honeypotField = document.createElement('input');
      honeypotField.type = 'text';
      honeypotField.name = 'website';
      honeypotField.setAttribute('data-rx-honeypot', 'true');
      form.appendChild(honeypotField);
    }

    honeypotField.setAttribute('tabindex', '-1');
    honeypotField.setAttribute('autocomplete', 'off');
    honeypotField.setAttribute('aria-hidden', 'true');

    honeypotField.style.position = 'absolute';
    honeypotField.style.left = '-9999px';
    honeypotField.style.width = '1px';
    honeypotField.style.height = '1px';
    honeypotField.style.opacity = '0';
    honeypotField.style.pointerEvents = 'none';
  }

  /**
   * ------------------------------------------------------------
   * 12. Accessibility setup
   * ------------------------------------------------------------
   */

  function prepareAccessibility(form, index) {
    var messageElement = getMessageElement(form);
    var emailField = safeQuery(form, RXNewsletterConfig.emailSelector);

    var messageId = messageElement.id || 'rx-newsletter-message-' + index;
    messageElement.id = messageId;

    setAttr(messageElement, 'aria-live', 'polite');

    if (emailField) {
      setAttr(emailField, 'aria-describedby', messageId);
      setAttr(emailField, 'autocomplete', 'email');

      if (!emailField.getAttribute('inputmode')) {
        setAttr(emailField, 'inputmode', 'email');
      }
    }
  }

  /**
   * ------------------------------------------------------------
   * 13. Initialize forms
   * ------------------------------------------------------------
   */

  function initNewsletterForm(form, index) {
    if (!form || form.getAttribute('data-rx-newsletter-ready') === 'true') {
      return;
    }

    form.setAttribute('data-rx-newsletter-ready', 'true');

    prepareHoneypot(form);
    prepareAccessibility(form, index);

    form.addEventListener('submit', handleNewsletterSubmit, false);

    var inputs = safeQueryAll(form, 'input, textarea, select');

    inputs.forEach(function (input) {
      input.addEventListener('input', handleFieldInput, false);
      input.addEventListener('change', handleFieldInput, false);
    });

    addClass(form, RXNewsletterConfig.classNames.validated);

    dispatchRXEvent('rx:newsletter:ready', {
      form: form
    });

    log('Newsletter form ready:', form);
  }

  function initAllNewsletterForms() {
    var forms = safeQueryAll(document, RXNewsletterConfig.formSelector);

    forms.forEach(function (form, index) {
      initNewsletterForm(form, index + 1);
    });

    log('Total newsletter forms:', forms.length);
  }

  /**
   * ------------------------------------------------------------
   * 14. Mutation observer for dynamically loaded forms
   * ------------------------------------------------------------
   */

  function initMutationObserver() {
    if (!window.MutationObserver) {
      return;
    }

    var observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        var nodes = Array.prototype.slice.call(mutation.addedNodes || []);

        nodes.forEach(function (node) {
          if (!node || node.nodeType !== 1) {
            return;
          }

          if (matchesSelector(node, RXNewsletterConfig.formSelector)) {
            initNewsletterForm(node, document.querySelectorAll(RXNewsletterConfig.formSelector).length);
            return;
          }

          var nestedForms = safeQueryAll(node, RXNewsletterConfig.formSelector);

          nestedForms.forEach(function (form, index) {
            initNewsletterForm(form, index + 1);
          });
        });
      });
    });

    observer.observe(document.documentElement || document.body, {
      childList: true,
      subtree: true
    });
  }

  /**
   * ------------------------------------------------------------
   * 15. Public API
   * ------------------------------------------------------------
   */

  window.RXNewsletter = {
    config: RXNewsletterConfig,

    init: function () {
      initAllNewsletterForms();
    },

    validate: function (form) {
      return validateNewsletterForm(form);
    },

    submit: function (form) {
      if (!form) {
        return;
      }

      var fakeEvent = {
        currentTarget: form,
        preventDefault: function () {}
      };

      handleNewsletterSubmit(fakeEvent);
    },

    clearStorage: function () {
      if (!canUseLocalStorage) {
        return;
      }

      var keysToRemove = [];
      var i;

      for (i = 0; i < window.localStorage.length; i++) {
        var key = window.localStorage.key(i);

        if (key && key.indexOf(RXNewsletterConfig.storagePrefix) === 0) {
          keysToRemove.push(key);
        }
      }

      keysToRemove.forEach(function (key) {
        window.localStorage.removeItem(key);
      });
    },

    resetCooldown: function () {
      storageRemove('last_submit_time');
    }
  };

  /**
   * ------------------------------------------------------------
   * 16. DOM ready
   * ------------------------------------------------------------
   */

  function ready(callback) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', callback, false);
    } else {
      callback();
    }
  }

  ready(function () {
    initAllNewsletterForms();
    initMutationObserver();

    dispatchRXEvent('rx:newsletter:chunk_loaded', {
      config: RXNewsletterConfig
    });
  });

})();

Recommended simple HTML

<form class="rx-newsletter-form" data-rx-newsletter-form data-source="footer">
  <label for="rx-newsletter-email">Subscribe to RX Health Updates</label>

  <input
    id="rx-newsletter-email"
    type="email"
    name="email"
    placeholder="Enter your email"
    required
  >

  <input
    type="text"
    name="name"
    placeholder="Your name"
  >

  <label>
    <input type="checkbox" name="consent" required>
    I agree to receive health updates.
  </label>

  <button type="submit">Subscribe</button>

  <div class="rx-newsletter-message" data-rx-newsletter-message></div>
</form>

Important

This JavaScript is frontend-ready, but real subscription needs a backend endpoint.

Later you can connect it with:

wp_localize_script(
    'rx-newsletter-chunk',
    'RX_NEWSLETTER_CONFIG',
    array(
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'restUrl' => esc_url_raw(rest_url('rx/v1/newsletter')),
        'nonce'   => wp_create_nonce('wp_rest'),
    )
);

Best file name is exactly:

chunk-024-newsletter.js

Best handle name:

rx-theme-newsletter

Best enqueue condition: load only where newsletter form exists, such as footer, sidebar, homepage, blog single, or popup newsletter area.

Leave a Reply

Your email address will not be published. Required fields are marked *