LESSON 6 โฑ๏ธ 15 min read

JavaScript Integration: Building Dynamic Interfaces

Setting Up JavaScript for API Access

Enqueuing Scripts with API Data

function myplugin_enqueue_scripts() {
    wp_enqueue_script(
        'myplugin-app',
        plugins_url('js/app.js', __FILE__),
        ['wp-api-fetch'],  // Use WordPress's built-in fetch wrapper
        '1.0.0',
        true
    );
    
    wp_localize_script('myplugin-app', 'myPluginApi', [
        'root'  => esc_url_raw(rest_url()),
        'nonce' => wp_create_nonce('wp_rest'),
        'userId' => get_current_user_id()
    ]);
}
add_action('wp_enqueue_scripts', 'myplugin_enqueue_scripts');

Using wp.apiFetch

WordPress includes wp-api-fetch for authenticated requests:

// If wp-api-fetch is available
wp.apiFetch({ path: '/wp/v2/posts' }).then(posts => {
    console.log(posts);
});

// With parameters
wp.apiFetch({
    path: '/wp/v2/posts',
    method: 'POST',
    data: {
        title: 'New Post',
        content: 'Content here',
        status: 'publish'
    }
}).then(post => {
    console.log('Created:', post);
});

Building a Task Manager UI

HTML Structure

<div id="task-app">
    <div class="task-header">
        <h2>My Tasks</h2>
        <button id="add-task-btn" class="button button-primary">Add Task</button>
    </div>
    
    <div class="task-filters">
        <button data-filter="all" class="active">All</button>
        <button data-filter="pending">Pending</button>
        <button data-filter="completed">Completed</button>
    </div>
    
    <div id="task-list" class="task-list">
        <!-- Tasks loaded dynamically -->
    </div>
    
    <div id="task-modal" class="modal" style="display:none;">
        <div class="modal-content">
            <h3>Add Task</h3>
            <input type="text" id="task-title" placeholder="Task title">
            <button id="save-task">Save</button>
            <button id="cancel-task">Cancel</button>
        </div>
    </div>
</div>

JavaScript Application

(function() {
    'use strict';
    
    const API = {
        baseUrl: myPluginApi.root + 'taskmanager/v1',
        
        async request(endpoint, options = {}) {
            const response = await fetch(this.baseUrl + endpoint, {
                ...options,
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': myPluginApi.nonce,
                    ...options.headers
                }
            });
            
            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.message || 'API request failed');
            }
            
            return response.json();
        },
        
        getTasks(filter = '') {
            const params = filter ? `?status=${filter}` : '';
            return this.request(`/tasks${params}`);
        },
        
        createTask(title) {
            return this.request('/tasks', {
                method: 'POST',
                body: JSON.stringify({ title })
            });
        },
        
        updateTask(id, data) {
            return this.request(`/tasks/${id}`, {
                method: 'PUT',
                body: JSON.stringify(data)
            });
        },
        
        deleteTask(id) {
            return this.request(`/tasks/${id}`, {
                method: 'DELETE'
            });
        }
    };
    
    const TaskApp = {
        currentFilter: 'all',
        
        init() {
            this.bindEvents();
            this.loadTasks();
        },
        
        bindEvents() {
            // Add task button
            document.getElementById('add-task-btn').addEventListener('click', () => {
                this.showModal();
            });
            
            // Filter buttons
            document.querySelectorAll('.task-filters button').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    this.setFilter(e.target.dataset.filter);
                });
            });
            
            // Save task
            document.getElementById('save-task').addEventListener('click', () => {
                this.saveTask();
            });
            
            // Cancel
            document.getElementById('cancel-task').addEventListener('click', () => {
                this.hideModal();
            });
            
            // Task list delegation
            document.getElementById('task-list').addEventListener('click', (e) => {
                const taskEl = e.target.closest('.task-item');
                if (!taskEl) return;
                
                const taskId = taskEl.dataset.id;
                
                if (e.target.matches('.complete-btn')) {
                    this.toggleComplete(taskId);
                } else if (e.target.matches('.delete-btn')) {
                    this.deleteTask(taskId);
                }
            });
        },
        
        async loadTasks() {
            const taskList = document.getElementById('task-list');
            taskList.innerHTML = '<div class="loading">Loading...</div>';
            
            try {
                const filter = this.currentFilter === 'all' ? '' : this.currentFilter;
                const tasks = await API.getTasks(filter);
                this.renderTasks(tasks);
            } catch (error) {
                taskList.innerHTML = `<div class="error">${error.message}</div>`;
            }
        },
        
        renderTasks(tasks) {
            const taskList = document.getElementById('task-list');
            
            if (tasks.length === 0) {
                taskList.innerHTML = '<div class="empty">No tasks found</div>';
                return;
            }
            
            taskList.innerHTML = tasks.map(task => `
                <div class="task-item ${task.status}" data-id="${task.id}">
                    <span class="task-title">${this.escapeHtml(task.title)}</span>
                    <div class="task-actions">
                        <button class="complete-btn">
                            ${task.status === 'completed' ? 'โ†ฉ๏ธ Undo' : 'โœ… Complete'}
                        </button>
                        <button class="delete-btn">๐Ÿ—‘๏ธ Delete</button>
                    </div>
                </div>
            `).join('');
        },
        
        async saveTask() {
            const input = document.getElementById('task-title');
            const title = input.value.trim();
            
            if (!title) {
                alert('Please enter a task title');
                return;
            }
            
            try {
                await API.createTask(title);
                input.value = '';
                this.hideModal();
                this.loadTasks();
            } catch (error) {
                alert('Failed to create task: ' + error.message);
            }
        },
        
        async toggleComplete(id) {
            const taskEl = document.querySelector(`[data-id="${id}"]`);
            const isCompleted = taskEl.classList.contains('completed');
            
            try {
                await API.updateTask(id, {
                    status: isCompleted ? 'pending' : 'completed'
                });
                this.loadTasks();
            } catch (error) {
                alert('Failed to update task: ' + error.message);
            }
        },
        
        async deleteTask(id) {
            if (!confirm('Delete this task?')) return;
            
            try {
                await API.deleteTask(id);
                this.loadTasks();
            } catch (error) {
                alert('Failed to delete task: ' + error.message);
            }
        },
        
        setFilter(filter) {
            this.currentFilter = filter;
            
            document.querySelectorAll('.task-filters button').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.filter === filter);
            });
            
            this.loadTasks();
        },
        
        showModal() {
            document.getElementById('task-modal').style.display = 'flex';
            document.getElementById('task-title').focus();
        },
        
        hideModal() {
            document.getElementById('task-modal').style.display = 'none';
        },
        
        escapeHtml(str) {
            const div = document.createElement('div');
            div.textContent = str;
            return div.innerHTML;
        }
    };
    
    document.addEventListener('DOMContentLoaded', () => TaskApp.init());
})();

CSS Styling

.task-app {
    max-width: 600px;
    margin: 2rem auto;
    font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}

.task-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
}

.task-filters {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.task-filters button {
    padding: 0.5rem 1rem;
    border: 1px solid #ddd;
    background: #fff;
    cursor: pointer;
    border-radius: 4px;
}

.task-filters button.active {
    background: #0073aa;
    color: #fff;
    border-color: #0073aa;
}

.task-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border: 1px solid #ddd;
    margin-bottom: 0.5rem;
    border-radius: 4px;
    background: #fff;
}

.task-item.completed .task-title {
    text-decoration: line-through;
    opacity: 0.6;
}

.modal {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal-content {
    background: #fff;
    padding: 2rem;
    border-radius: 8px;
    min-width: 300px;
}
Tip: For complex applications, consider using React, Vue, or Alpine.js for better state management.

Next Steps

In the next lesson, we'll optimize API performance with caching, rate limiting, and best practices.

๐ŸŽฏ Lesson Complete! You can now build interactive JavaScript interfaces powered by the REST API.