Table of Contents

AJAX Deferred HTML System

Dynamic Content Loading with Placeholder Replacement

Jankx 2.0 sử dụng hệ thống AJAX deferred HTML để tải dynamic content với placeholders, tránh vấn đề cache và tối ưu performance.

🏗 AJAX System Architecture

System Flow

┌─────────────────────────────────────┐
│         Initial Render              │
│  ┌─────────────┐  ┌─────────────┐  │
│  │   Static    │  │  Placeholder│  │
│  │  Content    │  │   Content   │  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│         AJAX Request                │
│  ┌─────────────┐  ┌─────────────┐  │
│  │   Client    │  │   Server    │  │
│  │   Request   │  │  Processing │  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│         Content Replacement         │
│  ┌─────────────┐  ┌─────────────┐  │
│  │  Placeholder│  │  Dynamic    │  │
│  │ Replacement │  │  Content    │  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘

🔧 Placeholder System

Placeholder Generation

<?php
namespace Jankx\Gutenberg\AJAX;

class PlaceholderGenerator
{
    private $placeholderId = 0;

    public function generatePlaceholder(string $blockName, array $attributes = []): string
    {
        $this->placeholderId++;
        $placeholderId = "jankx-placeholder-{$this->placeholderId}";

        $placeholderData = [
            'id' => $placeholderId,
            'block' => $blockName,
            'attributes' => $attributes,
            'nonce' => wp_create_nonce('jankx_ajax_nonce'),
        ];

        $encodedData = base64_encode(json_encode($placeholderData));

        return sprintf(
            '<div class="jankx-placeholder" data-placeholder="%s" data-block="%s">
                <div class="placeholder-content">
                    <div class="placeholder-spinner"></div>
                    <div class="placeholder-text">Loading...</div>
                </div>
            </div>',
            esc_attr($encodedData),
            esc_attr($blockName)
        );
    }

    public function generateSkeletonPlaceholder(string $blockName, array $attributes = []): string
    {
        $this->placeholderId++;
        $placeholderId = "jankx-skeleton-{$this->placeholderId}";

        return sprintf(
            '<div class="jankx-skeleton-placeholder" data-placeholder="%s" data-block="%s">
                <div class="skeleton-content">
                    <div class="skeleton-line"></div>
                    <div class="skeleton-line"></div>
                    <div class="skeleton-line"></div>
                </div>
            </div>',
            esc_attr($this->generatePlaceholderData($blockName, $attributes)),
            esc_attr($blockName)
        );
    }

    private function generatePlaceholderData(string $blockName, array $attributes): string
    {
        $data = [
            'block' => $blockName,
            'attributes' => $attributes,
            'nonce' => wp_create_nonce('jankx_ajax_nonce'),
        ];

        return base64_encode(json_encode($data));
    }
}

Placeholder Rendering

class PlaceholderRenderer
{
    private $generator;

    public function __construct(PlaceholderGenerator $generator)
    {
        $this->generator = $generator;
    }

    public function renderPlaceholder(string $blockName, array $attributes = []): string
    {
        // Check if content should be deferred
        if ($this->shouldDeferContent($blockName, $attributes)) {
            return $this->generator->generatePlaceholder($blockName, $attributes);
        }

        // Render content immediately
        return $this->renderContent($blockName, $attributes);
    }

    private function shouldDeferContent(string $blockName, array $attributes): bool
    {
        $deferredBlocks = [
            'jankx/testimonials-grid',
            'jankx/posts-grid',
            'jankx/comments-section',
            'jankx/search-results',
            'jankx/dynamic-form',
        ];

        // Check if block should be deferred
        if (in_array($blockName, $deferredBlocks)) {
            return true;
        }

        // Check for heavy content
        if ($this->isHeavyContent($attributes)) {
            return true;
        }

        // Check for user-specific content
        if ($this->isUserSpecificContent($attributes)) {
            return true;
        }

        return false;
    }

    private function isHeavyContent(array $attributes): bool
    {
        // Check for large datasets
        if (isset($attributes['postsPerPage']) && $attributes['postsPerPage'] > 6) {
            return true;
        }

        // Check for complex queries
        if (isset($attributes['complexQuery']) && $attributes['complexQuery']) {
            return true;
        }

        return false;
    }

    private function isUserSpecificContent(array $attributes): bool
    {
        // Check for user-specific data
        if (isset($attributes['showUserData']) && $attributes['showUserData']) {
            return true;
        }

        // Check for personalized content
        if (isset($attributes['personalized']) && $attributes['personalized']) {
            return true;
        }

        return false;
    }
}

🔄 AJAX Handler

AJAX Request Handler

<?php
namespace Jankx\Gutenberg\AJAX;

class AJAXHandler
{
    private $contentRenderer;
    private $securityManager;

    public function __construct(ContentRenderer $contentRenderer, SecurityManager $securityManager)
    {
        $this->contentRenderer = $contentRenderer;
        $this->securityManager = $securityManager;
    }

    public function init(): void
    {
        add_action('wp_ajax_jankx_load_content', [$this, 'handleLoadContent']);
        add_action('wp_ajax_nopriv_jankx_load_content', [$this, 'handleLoadContent']);
    }

    public function handleLoadContent(): void
    {
        // Verify nonce
        if (!wp_verify_nonce($_POST['nonce'], 'jankx_ajax_nonce')) {
            wp_die('Security check failed');
        }

        // Validate request data
        $blockName = sanitize_text_field($_POST['block'] ?? '');
        $attributes = $this->sanitizeAttributes($_POST['attributes'] ?? []);
        $placeholderId = sanitize_text_field($_POST['placeholder_id'] ?? '');

        if (empty($blockName)) {
            wp_send_json_error('Invalid block name');
        }

        // Render content
        try {
            $content = $this->contentRenderer->renderContent($blockName, $attributes);

            wp_send_json_success([
                'content' => $content,
                'placeholder_id' => $placeholderId,
                'block' => $blockName,
            ]);
        } catch (\Exception $e) {
            wp_send_json_error('Failed to render content: ' . $e->getMessage());
        }
    }

    private function sanitizeAttributes(array $attributes): array
    {
        $sanitized = [];

        foreach ($attributes as $key => $value) {
            if (is_string($value)) {
                $sanitized[$key] = sanitize_text_field($value);
            } elseif (is_array($value)) {
                $sanitized[$key] = $this->sanitizeAttributes($value);
            } else {
                $sanitized[$key] = $value;
            }
        }

        return $sanitized;
    }
}

Content Renderer

class ContentRenderer
{
    private $blockRegistry;
    private $templateRenderer;

    public function __construct(BlockRegistry $blockRegistry, TemplateRenderer $templateRenderer)
    {
        $this->blockRegistry = $blockRegistry;
        $this->templateRenderer = $templateRenderer;
    }

    public function renderContent(string $blockName, array $attributes = []): string
    {
        // Get block instance
        $block = $this->blockRegistry->getBlock($blockName);

        if (!$block) {
            throw new \Exception("Block not found: {$blockName}");
        }

        // Render content
        $content = $block->render($attributes, '');

        // Apply filters
        $content = apply_filters('jankx_ajax_content', $content, $blockName, $attributes);

        return $content;
    }

    public function renderTestimonialsGrid(array $attributes): string
    {
        $postsPerPage = $attributes['postsPerPage'] ?? 6;
        $category = $attributes['category'] ?? '';

        $args = [
            'post_type' => 'testimonial',
            'posts_per_page' => $postsPerPage,
            'post_status' => 'publish',
        ];

        if (!empty($category)) {
            $args['tax_query'] = [
                [
                    'taxonomy' => 'testimonial_category',
                    'field' => 'slug',
                    'terms' => $category,
                ]
            ];
        }

        $testimonials = get_posts($args);

        return $this->templateRenderer->render('testimonials-grid', [
            'testimonials' => $testimonials,
            'attributes' => $attributes,
        ]);
    }

    public function renderPostsGrid(array $attributes): string
    {
        $postsPerPage = $attributes['postsPerPage'] ?? 6;
        $category = $attributes['category'] ?? '';
        $postType = $attributes['postType'] ?? 'post';

        $args = [
            'post_type' => $postType,
            'posts_per_page' => $postsPerPage,
            'post_status' => 'publish',
        ];

        if (!empty($category)) {
            $args['category_name'] = $category;
        }

        $posts = get_posts($args);

        return $this->templateRenderer->render('posts-grid', [
            'posts' => $posts,
            'attributes' => $attributes,
        ]);
    }
}

🎯 Client-Side Implementation

JavaScript AJAX Handler

// assets/js/ajax-handler.js
class JankxAJAXHandler {
    constructor() {
        this.init();
    }

    init() {
        this.observePlaceholders();
        this.bindEvents();
    }

    observePlaceholders() {
        // Use Intersection Observer for lazy loading
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    this.loadPlaceholder(entry.target);
                    observer.unobserve(entry.target);
                }
            });
        }, {
            rootMargin: '50px',
            threshold: 0.1
        });

        // Observe all placeholders
        document.querySelectorAll('.jankx-placeholder').forEach(placeholder => {
            observer.observe(placeholder);
        });
    }

    bindEvents() {
        // Handle manual refresh
        document.addEventListener('click', (e) => {
            if (e.target.matches('.jankx-refresh-content')) {
                e.preventDefault();
                this.refreshContent(e.target.closest('.jankx-placeholder'));
            }
        });
    }

    async loadPlaceholder(placeholder) {
        try {
            // Show loading state
            this.showLoading(placeholder);

            // Get placeholder data
            const placeholderData = this.getPlaceholderData(placeholder);

            // Make AJAX request
            const response = await this.makeRequest(placeholderData);

            // Replace content
            this.replaceContent(placeholder, response.content);

            // Trigger events
            this.triggerEvents(placeholder, response);

        } catch (error) {
            console.error('Failed to load placeholder:', error);
            this.showError(placeholder, error.message);
        }
    }

    getPlaceholderData(placeholder) {
        const encodedData = placeholder.dataset.placeholder;
        const blockName = placeholder.dataset.block;

        try {
            const data = JSON.parse(atob(encodedData));
            return {
                block: blockName,
                attributes: data.attributes || {},
                nonce: data.nonce,
                placeholder_id: placeholder.id
            };
        } catch (error) {
            throw new Error('Invalid placeholder data');
        }
    }

    async makeRequest(data) {
        const formData = new FormData();
        formData.append('action', 'jankx_load_content');
        formData.append('block', data.block);
        formData.append('attributes', JSON.stringify(data.attributes));
        formData.append('nonce', data.nonce);
        formData.append('placeholder_id', data.placeholder_id);

        const response = await fetch(jankx_ajax.ajax_url, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });

        if (!response.ok) {
            throw new Error('Network error');
        }

        const result = await response.json();

        if (!result.success) {
            throw new Error(result.data || 'Request failed');
        }

        return result.data;
    }

    replaceContent(placeholder, content) {
        // Create wrapper for new content
        const wrapper = document.createElement('div');
        wrapper.className = 'jankx-loaded-content';
        wrapper.innerHTML = content;

        // Replace placeholder with content
        placeholder.parentNode.replaceChild(wrapper, placeholder);

        // Initialize any scripts in the new content
        this.initializeScripts(wrapper);
    }

    showLoading(placeholder) {
        placeholder.innerHTML = `
            <div class="jankx-loading">
                <div class="loading-spinner"></div>
                <div class="loading-text">Loading content...</div>
            </div>
        `;
    }

    showError(placeholder, message) {
        placeholder.innerHTML = `
            <div class="jankx-error">
                <div class="error-icon">⚠️</div>
                <div class="error-message">${message}</div>
                <button class="jankx-retry-btn" onclick="jankxAJAX.retryLoad(this)">
                    Retry
                </button>
            </div>
        `;
    }

    triggerEvents(placeholder, response) {
        // Trigger custom event
        const event = new CustomEvent('jankx:contentLoaded', {
            detail: {
                placeholder: placeholder,
                response: response,
                block: response.block
            }
        });

        document.dispatchEvent(event);
    }

    initializeScripts(wrapper) {
        // Find and execute any scripts in the new content
        const scripts = wrapper.querySelectorAll('script');
        scripts.forEach(script => {
            if (script.src) {
                // External script
                const newScript = document.createElement('script');
                newScript.src = script.src;
                document.head.appendChild(newScript);
            } else {
                // Inline script
                eval(script.textContent);
            }
        });
    }

    retryLoad(button) {
        const placeholder = button.closest('.jankx-error');
        if (placeholder) {
            this.loadPlaceholder(placeholder);
        }
    }
}

// Initialize AJAX handler
const jankxAJAX = new JankxAJAXHandler();

🎨 CSS for Placeholders

Placeholder Styles

// assets/css/placeholders.scss
.jankx-placeholder {
    position: relative;
    min-height: 200px;
    background: #f8f9fa;
    border-radius: 8px;
    overflow: hidden;

    .placeholder-content {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100%;
        padding: 2rem;
    }

    .placeholder-spinner {
        width: 40px;
        height: 40px;
        border: 3px solid #e9ecef;
        border-top: 3px solid #007cba;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        margin-bottom: 1rem;
    }

    .placeholder-text {
        color: #6c757d;
        font-size: 14px;
        text-align: center;
    }
}

.jankx-skeleton-placeholder {
    .skeleton-content {
        padding: 1rem;
    }

    .skeleton-line {
        height: 16px;
        background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
        background-size: 200% 100%;
        animation: shimmer 1.5s infinite;
        margin-bottom: 12px;
        border-radius: 4px;

        &:last-child {
            width: 60%;
        }
    }
}

.jankx-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem;

    .loading-spinner {
        width: 32px;
        height: 32px;
        border: 2px solid #e9ecef;
        border-top: 2px solid #007cba;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        margin-bottom: 1rem;
    }

    .loading-text {
        color: #6c757d;
        font-size: 14px;
    }
}

.jankx-error {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    text-align: center;

    .error-icon {
        font-size: 2rem;
        margin-bottom: 1rem;
    }

    .error-message {
        color: #dc3545;
        margin-bottom: 1rem;
    }

    .jankx-retry-btn {
        background: #007cba;
        color: white;
        border: none;
        padding: 0.5rem 1rem;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;

        &:hover {
            background: #005a87;
        }
    }
}

.jankx-loaded-content {
    animation: fadeIn 0.3s ease-in-out;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

@keyframes shimmer {
    0% { background-position: -200% 0; }
    100% { background-position: 200% 0; }
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

🔒 Security Implementation

Security Manager

class SecurityManager
{
    public function validateAJAXRequest(): bool
    {
        // Check nonce
        if (!wp_verify_nonce($_POST['nonce'], 'jankx_ajax_nonce')) {
            return false;
        }

        // Check user permissions
        if (!$this->checkUserPermissions()) {
            return false;
        }

        // Rate limiting
        if ($this->isRateLimited()) {
            return false;
        }

        return true;
    }

    private function checkUserPermissions(): bool
    {
        // Check if user can access the content
        $blockName = $_POST['block'] ?? '';

        switch ($blockName) {
            case 'jankx/admin-dashboard':
                return current_user_can('manage_options');
            case 'jankx/user-profile':
                return is_user_logged_in();
            default:
                return true;
        }
    }

    private function isRateLimited(): bool
    {
        $ip = $this->getClientIP();
        $key = "jankx_ajax_rate_limit_{$ip}";
        $limit = 100; // requests per hour
        $window = 3600; // 1 hour

        $requests = get_transient($key) ?: 0;

        if ($requests >= $limit) {
            return true;
        }

        set_transient($key, $requests + 1, $window);
        return false;
    }

    private function getClientIP(): string
    {
        $ipKeys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];

        foreach ($ipKeys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                foreach (explode(',', $_SERVER[$key]) as $ip) {
                    $ip = trim($ip);
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
                        return $ip;
                    }
                }
            }
        }

        return $_SERVER['REMOTE_ADDR'] ?? '';
    }
}

Next: Layout System Frontend Rendering