⚡ 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
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