Skip to main content

Course Progress

Loading...

⚡ JavaScript in WordPress Themes

Master JavaScript implementation in WordPress themes

Learn proper enqueuing, AJAX, REST API, and modern JavaScript patterns

Learning Objectives

  • Properly enqueue JavaScript in WordPress
  • Understand script dependencies and loading order
  • Implement AJAX in WordPress themes
  • Work with the WordPress REST API
  • Use wp_localize_script for data passing
  • Create modular JavaScript architecture
  • Handle WordPress JavaScript hooks
  • Debug JavaScript in WordPress

JavaScript in WordPress Context

WordPress has specific methods for including and managing JavaScript to ensure proper loading order, prevent conflicts, and maintain compatibility with plugins and themes.

💡
Key Principle
Never hardcode script tags in WordPress themes. Always use wp_enqueue_script() to manage JavaScript files properly.

Enqueuing Scripts Properly

Basic Script Enqueuing

<?php
// functions.php

function mytheme_enqueue_scripts() {
    // Get theme version for cache busting
    $theme_version = wp_get_theme()->get( 'Version' );
    
    // Enqueue main theme script
    wp_enqueue_script(
        'mytheme-script',                                    // Handle
        get_template_directory_uri() . '/assets/js/main.js', // Source
        array(),                                              // Dependencies
        $theme_version,                                       // Version
        true                                                  // In footer
    );
    
    // Enqueue navigation script with dependencies
    wp_enqueue_script(
        'mytheme-navigation',
        get_template_directory_uri() . '/assets/js/navigation.js',
        array( 'mytheme-script' ),  // Depends on main script
        $theme_version,
        true
    );
    
    // Conditional script loading
    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }
    
    // Load script only on specific pages
    if ( is_page_template( 'template-contact.php' ) ) {
        wp_enqueue_script(
            'mytheme-contact',
            get_template_directory_uri() . '/assets/js/contact.js',
            array( 'jquery' ),
            $theme_version,
            true
        );
    }
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_scripts' );

// Dequeue scripts (removing unwanted scripts)
function mytheme_dequeue_scripts() {
    // Remove unnecessary plugin script on non-required pages
    if ( ! is_page( 'contact' ) ) {
        wp_dequeue_script( 'contact-form-7' );
        wp_deregister_script( 'contact-form-7' );
    }
}
add_action( 'wp_print_scripts', 'mytheme_dequeue_scripts', 100 );
Parameter Description Example
$handle Unique name for the script 'mytheme-main'
$src URL to the script file get_template_directory_uri() . '/js/main.js'
$deps Array of script dependencies array('jquery', 'underscore')
$ver Script version for cache busting '1.0.0' or filemtime()
$in_footer Load in footer (true) or header (false) true (recommended)

Localizing Scripts (Passing Data to JavaScript)

wp_localize_script() Usage

<?php
function mytheme_enqueue_scripts() {
    // Enqueue the script first
    wp_enqueue_script(
        'mytheme-ajax',
        get_template_directory_uri() . '/assets/js/ajax.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
    
    // Localize the script with data
    wp_localize_script( 'mytheme-ajax', 'mytheme_ajax_object', array(
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'mytheme-ajax-nonce' ),
        'site_url' => home_url(),
        'theme_url' => get_template_directory_uri(),
        'is_user_logged_in' => is_user_logged_in(),
        'current_user_id' => get_current_user_id(),
        'post_id' => get_the_ID(),
        'strings' => array(
            'loading' => __( 'Loading...', 'mytheme' ),
            'error'   => __( 'An error occurred', 'mytheme' ),
            'success' => __( 'Success!', 'mytheme' ),
            'confirm' => __( 'Are you sure?', 'mytheme' ),
        ),
        'settings' => array(
            'animation_speed' => 300,
            'autoplay' => true,
            'items_per_page' => get_option( 'posts_per_page' ),
        )
    ) );
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_scripts' );

Accessing Localized Data in JavaScript

// ajax.js
(function($) {
    'use strict';
    
    // Access localized data
    console.log(mytheme_ajax_object.ajax_url);
    console.log(mytheme_ajax_object.strings.loading);
    
    // Use in AJAX request
    $.ajax({
        url: mytheme_ajax_object.ajax_url,
        type: 'POST',
        data: {
            action: 'mytheme_load_more',
            nonce: mytheme_ajax_object.nonce,
            post_id: mytheme_ajax_object.post_id
        },
        beforeSend: function() {
            $('#loading').text(mytheme_ajax_object.strings.loading);
        },
        success: function(response) {
            if (response.success) {
                $('#content').html(response.data);
            } else {
                alert(mytheme_ajax_object.strings.error);
            }
        }
    });
    
})(jQuery);

Implementing AJAX in WordPress

Complete AJAX Example - Load More Posts

<?php
// PHP: AJAX Handler (functions.php)

// For logged-in users
add_action( 'wp_ajax_load_more_posts', 'mytheme_load_more_posts' );
// For non-logged-in users
add_action( 'wp_ajax_nopriv_load_more_posts', 'mytheme_load_more_posts' );

function mytheme_load_more_posts() {
    // Verify nonce
    if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'load-more-nonce' ) ) {
        wp_die( 'Permission denied' );
    }
    
    // Get data from AJAX request
    $page = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1;
    $posts_per_page = isset( $_POST['posts_per_page'] ) ? intval( $_POST['posts_per_page'] ) : 10;
    $category = isset( $_POST['category'] ) ? sanitize_text_field( $_POST['category'] ) : '';
    
    // Query arguments
    $args = array(
        'post_type'      => 'post',
        'posts_per_page' => $posts_per_page,
        'paged'          => $page,
        'post_status'    => 'publish',
    );
    
    if ( ! empty( $category ) ) {
        $args['category_name'] = $category;
    }
    
    $query = new WP_Query( $args );
    
    $response = array();
    
    if ( $query->have_posts() ) {
        ob_start();
        
        while ( $query->have_posts() ) {
            $query->the_post();
            // Use your template part
            get_template_part( 'template-parts/content', get_post_format() );
        }
        
        $response['html'] = ob_get_clean();
        $response['max_pages'] = $query->max_num_pages;
        $response['found_posts'] = $query->found_posts;
        
        wp_send_json_success( $response );
    } else {
        wp_send_json_error( 'No more posts found' );
    }
    
    wp_die();
}

JavaScript: AJAX Request

// load-more.js
(function($) {
    'use strict';
    
    const LoadMore = {
        currentPage: 1,
        isLoading: false,
        
        init: function() {
            this.bindEvents();
        },
        
        bindEvents: function() {
            $('#load-more-btn').on('click', this.loadPosts.bind(this));
            
            // Infinite scroll
            $(window).on('scroll', this.infiniteScroll.bind(this));
        },
        
        loadPosts: function(e) {
            e.preventDefault();
            
            if (this.isLoading) return;
            
            const button = $(e.currentTarget);
            const container = $('#posts-container');
            
            this.isLoading = true;
            this.currentPage++;
            
            // Update button state
            button.addClass('loading').text('Loading...');
            
            $.ajax({
                url: mytheme_ajax.ajax_url,
                type: 'POST',
                dataType: 'json',
                data: {
                    action: 'load_more_posts',
                    nonce: mytheme_ajax.nonce,
                    page: this.currentPage,
                    posts_per_page: 10,
                    category: button.data('category')
                },
                success: (response) => {
                    if (response.success) {
                        // Append new posts with animation
                        const newPosts = $(response.data.html).hide();
                        container.append(newPosts);
                        newPosts.fadeIn();
                        
                        // Check if more posts available
                        if (this.currentPage >= response.data.max_pages) {
                            button.hide();
                            container.after('<p class="no-more-posts">No more posts to load</p>');
                        } else {
                            button.removeClass('loading').text('Load More');
                        }
                    } else {
                        console.error('Error:', response.data);
                    }
                    
                    this.isLoading = false;
                },
                error: (xhr, status, error) => {
                    console.error('AJAX Error:', error);
                    button.removeClass('loading').text('Error - Try Again');
                    this.isLoading = false;
                }
            });
        },
        
        infiniteScroll: function() {
            if (this.isLoading) return;
            
            const scrollPosition = $(window).scrollTop() + $(window).height();
            const contentHeight = $(document).height();
            
            if (scrollPosition > contentHeight - 200) {
                $('#load-more-btn').trigger('click');
            }
        }
    };
    
    $(document).ready(function() {
        LoadMore.init();
    });
    
})(jQuery);

Using WordPress REST API

Fetch API with WordPress REST API

// Modern JavaScript with Fetch API
class WordPressAPI {
    constructor() {
        this.apiUrl = `${window.location.origin}/wp-json/wp/v2`;
        this.nonce = mytheme_ajax.nonce;
    }
    
    // Get posts
    async getPosts(page = 1, perPage = 10, categories = []) {
        let url = `${this.apiUrl}/posts?page=${page}&per_page=${perPage}`;
        
        if (categories.length > 0) {
            url += `&categories=${categories.join(',')}`;
        }
        
        try {
            const response = await fetch(url, {
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': this.nonce
                }
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            const posts = await response.json();
            const totalPages = response.headers.get('X-WP-TotalPages');
            const total = response.headers.get('X-WP-Total');
            
            return {
                posts,
                totalPages: parseInt(totalPages),
                total: parseInt(total)
            };
        } catch (error) {
            console.error('Error fetching posts:', error);
            throw error;
        }
    }
    
    // Get single post
    async getPost(id) {
        try {
            const response = await fetch(`${this.apiUrl}/posts/${id}?_embed`, {
                headers: {
                    'X-WP-Nonce': this.nonce
                }
            });
            
            return await response.json();
        } catch (error) {
            console.error('Error fetching post:', error);
            throw error;
        }
    }
    
    // Create post (requires authentication)
    async createPost(data) {
        try {
            const response = await fetch(`${this.apiUrl}/posts`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': this.nonce
                },
                body: JSON.stringify({
                    title: data.title,
                    content: data.content,
                    status: 'draft',
                    categories: data.categories || [],
                    tags: data.tags || []
                })
            });
            
            return await response.json();
        } catch (error) {
            console.error('Error creating post:', error);
            throw error;
        }
    }
    
    // Search posts
    async searchPosts(searchTerm) {
        try {
            const response = await fetch(
                `${this.apiUrl}/search?search=${encodeURIComponent(searchTerm)}&type=post`,
                {
                    headers: {
                        'X-WP-Nonce': this.nonce
                    }
                }
            );
            
            return await response.json();
        } catch (error) {
            console.error('Error searching posts:', error);
            throw error;
        }
    }
}

// Usage
const api = new WordPressAPI();

// Get posts
api.getPosts(1, 10, [5, 8]).then(result => {
    console.log('Posts:', result.posts);
    console.log('Total pages:', result.totalPages);
    renderPosts(result.posts);
});

// Search posts
api.searchPosts('wordpress').then(results => {
    console.log('Search results:', results);
});

Custom REST API Endpoint

<?php
// Register custom REST API endpoint
add_action( 'rest_api_init', function () {
    register_rest_route( 'mytheme/v1', '/featured-posts', array(
        'methods'  => 'GET',
        'callback' => 'mytheme_get_featured_posts',
        'permission_callback' => '__return_true', // Public endpoint
    ) );
    
    register_rest_route( 'mytheme/v1', '/like-post/(?P<id>\d+)', array(
        'methods'  => 'POST',
        'callback' => 'mytheme_like_post',
        'permission_callback' => function() {
            return is_user_logged_in();
        },
        'args' => array(
            'id' => array(
                'validate_callback' => function($param, $request, $key) {
                    return is_numeric($param);
                }
            ),
        ),
    ) );
} );

function mytheme_get_featured_posts( $request ) {
    $args = array(
        'post_type' => 'post',
        'posts_per_page' => 5,
        'meta_key' => 'featured',
        'meta_value' => 'yes',
    );
    
    $posts = get_posts( $args );
    
    $data = array();
    
    foreach ( $posts as $post ) {
        $data[] = array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'excerpt' => $post->post_excerpt,
            'link' => get_permalink( $post->ID ),
            'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'medium' ),
        );
    }
    
    return new WP_REST_Response( $data, 200 );
}

function mytheme_like_post( $request ) {
    $post_id = $request['id'];
    $user_id = get_current_user_id();
    
    // Get current likes
    $likes = get_post_meta( $post_id, 'likes_count', true );
    $likes = $likes ? intval( $likes ) : 0;
    
    // Check if user already liked
    $liked_users = get_post_meta( $post_id, 'liked_users', true );
    $liked_users = $liked_users ? $liked_users : array();
    
    if ( in_array( $user_id, $liked_users ) ) {
        return new WP_REST_Response( array(
            'message' => 'Already liked',
            'likes' => $likes
        ), 400 );
    }
    
    // Add like
    $likes++;
    $liked_users[] = $user_id;
    
    update_post_meta( $post_id, 'likes_count', $likes );
    update_post_meta( $post_id, 'liked_users', $liked_users );
    
    return new WP_REST_Response( array(
        'message' => 'Post liked',
        'likes' => $likes
    ), 200 );
}

Modern JavaScript Module Pattern

ES6 Modules Structure

// modules/navigation.js
export class Navigation {
    constructor() {
        this.menu = document.querySelector('.site-navigation');
        this.toggleButton = document.querySelector('.menu-toggle');
        this.isOpen = false;
        
        this.init();
    }
    
    init() {
        if (!this.menu || !this.toggleButton) return;
        
        this.bindEvents();
        this.setupAccessibility();
    }
    
    bindEvents() {
        this.toggleButton.addEventListener('click', () => this.toggle());
        
        // Close on escape key
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && this.isOpen) {
                this.close();
            }
        });
        
        // Close on click outside
        document.addEventListener('click', (e) => {
            if (this.isOpen && !this.menu.contains(e.target)) {
                this.close();
            }
        });
    }
    
    toggle() {
        this.isOpen ? this.close() : this.open();
    }
    
    open() {
        this.isOpen = true;
        this.menu.classList.add('is-open');
        this.toggleButton.setAttribute('aria-expanded', 'true');
        
        // Trap focus
        this.trapFocus();
    }
    
    close() {
        this.isOpen = false;
        this.menu.classList.remove('is-open');
        this.toggleButton.setAttribute('aria-expanded', 'false');
        
        // Release focus
        this.releaseFocus();
    }
    
    trapFocus() {
        const focusableElements = this.menu.querySelectorAll(
            'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        
        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];
        
        this.menu.addEventListener('keydown', (e) => {
            if (e.key !== 'Tab') return;
            
            if (e.shiftKey && document.activeElement === firstElement) {
                e.preventDefault();
                lastElement.focus();
            } else if (!e.shiftKey && document.activeElement === lastElement) {
                e.preventDefault();
                firstElement.focus();
            }
        });
    }
    
    releaseFocus() {
        // Implementation for releasing focus trap
    }
    
    setupAccessibility() {
        // Add ARIA labels
        this.menu.setAttribute('aria-label', 'Main navigation');
        
        // Setup keyboard navigation
        const menuItems = this.menu.querySelectorAll('.menu-item-has-children > a');
        
        menuItems.forEach(item => {
            item.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    this.toggleSubmenu(item.parentElement);
                }
            });
        });
    }
    
    toggleSubmenu(menuItem) {
        const submenu = menuItem.querySelector('.sub-menu');
        if (!submenu) return;
        
        const isOpen = menuItem.classList.contains('is-open');
        
        if (isOpen) {
            menuItem.classList.remove('is-open');
            submenu.setAttribute('aria-hidden', 'true');
        } else {
            menuItem.classList.add('is-open');
            submenu.setAttribute('aria-hidden', 'false');
        }
    }
}

// modules/search.js
export class Search {
    constructor() {
        this.searchForm = document.querySelector('.search-form');
        this.searchInput = document.querySelector('.search-input');
        this.searchResults = document.querySelector('.search-results');
        this.debounceTimer = null;
        
        this.init();
    }
    
    init() {
        if (!this.searchInput) return;
        
        this.searchInput.addEventListener('input', (e) => {
            this.handleSearch(e.target.value);
        });
    }
    
    handleSearch(query) {
        clearTimeout(this.debounceTimer);
        
        if (query.length < 3) {
            this.hideResults();
            return;
        }
        
        this.debounceTimer = setTimeout(() => {
            this.performSearch(query);
        }, 300);
    }
    
    async performSearch(query) {
        try {
            const response = await fetch(
                `/wp-json/wp/v2/search?search=${encodeURIComponent(query)}&type=post&per_page=5`
            );
            
            const results = await response.json();
            this.displayResults(results);
        } catch (error) {
            console.error('Search error:', error);
        }
    }
    
    displayResults(results) {
        if (results.length === 0) {
            this.searchResults.innerHTML = '<p>No results found</p>';
            return;
        }
        
        const html = results.map(result => `
            <a href="${result.url}" class="search-result">
                <h4>${result.title}</h4>
                ${result.excerpt ? `<p>${result.excerpt}</p>` : ''}
            </a>
        `).join('');
        
        this.searchResults.innerHTML = html;
        this.showResults();
    }
    
    showResults() {
        this.searchResults.classList.add('is-visible');
    }
    
    hideResults() {
        this.searchResults.classList.remove('is-visible');
    }
}

// main.js
import { Navigation } from './modules/navigation.js';
import { Search } from './modules/search.js';

class Theme {
    constructor() {
        this.modules = {};
        this.init();
    }
    
    init() {
        // Wait for DOM ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => this.initModules());
        } else {
            this.initModules();
        }
    }
    
    initModules() {
        // Initialize modules
        this.modules.navigation = new Navigation();
        this.modules.search = new Search();
        
        // Initialize other features
        this.initSmoothScroll();
        this.initLazyLoading();
    }
    
    initSmoothScroll() {
        document.querySelectorAll('a[href^="#"]').forEach(anchor => {
            anchor.addEventListener('click', (e) => {
                e.preventDefault();
                const target = document.querySelector(anchor.getAttribute('href'));
                
                if (target) {
                    target.scrollIntoView({
                        behavior: 'smooth',
                        block: 'start'
                    });
                }
            });
        });
    }
    
    initLazyLoading() {
        if ('IntersectionObserver' in window) {
            const images = document.querySelectorAll('img[data-src]');
            const imageObserver = new IntersectionObserver((entries, observer) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const img = entry.target;
                        img.src = img.dataset.src;
                        img.removeAttribute('data-src');
                        imageObserver.unobserve(img);
                    }
                });
            });
            
            images.forEach(img => imageObserver.observe(img));
        }
    }
}

// Initialize theme
new Theme();

WordPress JavaScript Hooks

Common WordPress JavaScript Events

// WordPress admin events
(function($) {
    'use strict';
    
    // Customizer preview
    if (typeof wp !== 'undefined' && wp.customize) {
        wp.customize('blogname', function(value) {
            value.bind(function(newval) {
                $('.site-title').text(newval);
            });
        });
        
        wp.customize('header_textcolor', function(value) {
            value.bind(function(newval) {
                $('.site-title, .site-description').css({
                    'color': newval
                });
            });
        });
    }
    
    // Media uploader
    $(document).on('click', '.upload-button', function(e) {
        e.preventDefault();
        
        const button = $(this);
        const customUploader = wp.media({
            title: 'Select Image',
            button: {
                text: 'Use this image'
            },
            multiple: false
        });
        
        customUploader.on('select', function() {
            const attachment = customUploader.state().get('selection').first().toJSON();
            button.prev('.image-url').val(attachment.url);
            button.next('.image-preview').html('<img src="' + attachment.url + '">');
        });
        
        customUploader.open();
    });
    
    // Gutenberg block events
    if (window.wp && window.wp.data && window.wp.data.subscribe) {
        let previousPost = {};
        
        wp.data.subscribe(function() {
            const post = wp.data.select('core/editor').getCurrentPost();
            
            if (post.status !== previousPost.status) {
                console.log('Post status changed:', post.status);
            }
            
            if (post.title !== previousPost.title) {
                console.log('Post title changed:', post.title);
            }
            
            previousPost = post;
        });
    }
    
})(jQuery);

Debugging JavaScript in WordPress

Debug Techniques

// Debug helper object
const Debug = {
    enabled: true, // Set to false in production
    
    log: function(...args) {
        if (this.enabled && window.console && console.log) {
            console.log('[Theme Debug]:', ...args);
        }
    },
    
    error: function(...args) {
        if (this.enabled && window.console && console.error) {
            console.error('[Theme Error]:', ...args);
        }
    },
    
    table: function(data) {
        if (this.enabled && window.console && console.table) {
            console.table(data);
        }
    },
    
    time: function(label) {
        if (this.enabled && window.console && console.time) {
            console.time(label);
        }
    },
    
    timeEnd: function(label) {
        if (this.enabled && window.console && console.timeEnd) {
            console.timeEnd(label);
        }
    }
};

// Usage
Debug.log('Initializing theme');
Debug.time('Ajax Request');

// Check if script is loaded
if (typeof jQuery === 'undefined') {
    Debug.error('jQuery is not loaded');
}

// Check WordPress globals
Debug.log('Ajax URL:', mytheme_ajax.ajax_url);
Debug.log('Current User:', mytheme_ajax.current_user_id);

// Monitor AJAX requests
$(document).ajaxSend(function(event, xhr, settings) {
    Debug.log('AJAX Request:', settings.url, settings.data);
});

$(document).ajaxComplete(function(event, xhr, settings) {
    Debug.log('AJAX Response:', xhr.responseJSON);
});

// Performance monitoring
window.addEventListener('load', function() {
    if (window.performance && performance.timing) {
        const timing = performance.timing;
        const loadTime = timing.loadEventEnd - timing.navigationStart;
        Debug.log('Page load time:', loadTime + 'ms');
    }
});
Enable SCRIPT_DEBUG in wp-config.php during development to load non-minified versions of WordPress scripts for easier debugging.

Best Practices

JavaScript Best Practices in WordPress

  • Always enqueue scripts: Never hardcode script tags
  • Load in footer: Set $in_footer to true for better performance
  • Use dependencies: Properly declare script dependencies
  • Localize data: Use wp_localize_script for passing PHP data
  • Namespace your code: Avoid global scope pollution
  • Handle no-conflict mode: Wrap jQuery code properly
  • Check for existence: Verify elements exist before using them
  • Use modern JavaScript: Consider transpiling for older browsers
  • Optimize for performance: Minify and combine scripts for production
  • Progressive enhancement: Ensure functionality works without JavaScript
Always verify nonces in AJAX handlers to prevent CSRF attacks. Never trust data from the client side.

Practice Exercise

💻
Build Interactive Theme Features

Implement JavaScript functionality in your theme:

  1. Set up proper script enqueuing system
  2. Create AJAX load more posts functionality
  3. Implement live search with REST API
  4. Build interactive navigation menu
  5. Add smooth scrolling to anchors
  6. Create image lazy loading system
  7. Implement like/favorite system with AJAX
  8. Build comment form with AJAX submission
  9. Add dark mode toggle with localStorage
  10. Create modular JavaScript architecture

Additional Resources