LESSON 7 โฑ๏ธ 13 min read

Frontend Interactivity & View Scripts

View Scripts

View scripts run on the frontend only, perfect for interactive features. Define in block.json:

{
    "viewScript": "file:./view.js"
}

Basic View Script

// src/view.js
document.addEventListener( 'DOMContentLoaded', () => {
    const blocks = document.querySelectorAll( '.wp-block-theme-testimonial' );
    
    blocks.forEach( ( block ) => {
        const readMore = block.querySelector( '.read-more-btn' );
        const content = block.querySelector( '.testimonial-full' );
        
        if ( readMore && content ) {
            readMore.addEventListener( 'click', () => {
                content.classList.toggle( 'expanded' );
                readMore.textContent = content.classList.contains( 'expanded' )
                    ? 'Read Less'
                    : 'Read More';
            } );
        }
    } );
} );

Passing Data to View Scripts

Use data-* attributes in your Save component:

// save.js
export default function save( { attributes } ) {
    const { rating, animationDuration } = attributes;
    
    return (
        <div 
            { ...useBlockProps.save() } 
            data-rating={ rating }
            data-animation-duration={ animationDuration }
        >
            {/* content */}
        </div>
    );
}
// view.js
document.querySelectorAll( '.wp-block-theme-testimonial' ).forEach( ( block ) => {
    const rating = parseInt( block.dataset.rating, 10 );
    const duration = parseInt( block.dataset.animationDuration, 10 );
    
    // Use the data
    animateStars( block, rating, duration );
} );

Accordion Example

Full interactive accordion block:

// save.js
export default function save( { attributes } ) {
    const { items } = attributes;
    
    return (
        <div { ...useBlockProps.save() }>
            { items.map( ( item, index ) => (
                <div className="accordion-item" key={ index }>
                    <button 
                        className="accordion-header" 
                        aria-expanded="false"
                        aria-controls={ `panel-${ index }` }
                    >
                        { item.title }
                        <span className="accordion-icon">+</span>
                    </button>
                    <div 
                        className="accordion-panel" 
                        id={ `panel-${ index }` }
                        hidden
                    >
                        { item.content }
                    </div>
                </div>
            ) ) }
        </div>
    );
}
// view.js
document.addEventListener( 'DOMContentLoaded', () => {
    document.querySelectorAll( '.wp-block-theme-accordion' ).forEach( ( accordion ) => {
        const headers = accordion.querySelectorAll( '.accordion-header' );
        
        headers.forEach( ( header ) => {
            header.addEventListener( 'click', () => {
                const panel = document.getElementById( 
                    header.getAttribute( 'aria-controls' ) 
                );
                const isOpen = header.getAttribute( 'aria-expanded' ) === 'true';
                
                // Close all other panels (optional - for single open mode)
                headers.forEach( ( h ) => {
                    h.setAttribute( 'aria-expanded', 'false' );
                    h.querySelector( '.accordion-icon' ).textContent = '+';
                    document.getElementById( h.getAttribute( 'aria-controls' ) ).hidden = true;
                } );
                
                // Toggle current
                if ( ! isOpen ) {
                    header.setAttribute( 'aria-expanded', 'true' );
                    header.querySelector( '.accordion-icon' ).textContent = 'โˆ’';
                    panel.hidden = false;
                }
            } );
        } );
    } );
} );

The Interactivity API (WordPress 6.5+)

The modern way to add interactivity with declarative directives:

// block.json
{
    "supports": {
        "interactivity": true
    },
    "viewScriptModule": "file:./view.js"
}
// save.js
export default function save( { attributes } ) {
    return (
        <div 
            { ...useBlockProps.save() }
            data-wp-interactive="theme-blocks"
            data-wp-context={ JSON.stringify( { isOpen: false } ) }
        >
            <button data-wp-on--click="actions.toggle">
                <span data-wp-text="state.buttonText">Open</span>
            </button>
            <div data-wp-bind--hidden="!context.isOpen">
                Content here...
            </div>
        </div>
    );
}
// view.js
import { store, getContext } from '@wordpress/interactivity';

store( 'theme-blocks', {
    state: {
        get buttonText() {
            const context = getContext();
            return context.isOpen ? 'Close' : 'Open';
        },
    },
    actions: {
        toggle() {
            const context = getContext();
            context.isOpen = ! context.isOpen;
        },
    },
} );

Interactivity Directives

DirectivePurpose
data-wp-interactiveNamespace for the block
data-wp-contextLocal state for this instance
data-wp-on--clickEvent handler
data-wp-bind--hiddenBind attribute to state
data-wp-textSet text content
data-wp-class--activeToggle class
data-wp-watchRun callback on state change

Counter Example with Interactivity API

// save.js
export default function save( { attributes } ) {
    return (
        <div 
            { ...useBlockProps.save() }
            data-wp-interactive="theme-counter"
            data-wp-context='{ "count": 0 }'
        >
            <button data-wp-on--click="actions.decrement">โˆ’</button>
            <span data-wp-text="context.count">0</span>
            <button data-wp-on--click="actions.increment">+</button>
        </div>
    );
}
// view.js (as ES module)
import { store, getContext } from '@wordpress/interactivity';

store( 'theme-counter', {
    actions: {
        increment() {
            const context = getContext();
            context.count++;
        },
        decrement() {
            const context = getContext();
            if ( context.count > 0 ) {
                context.count--;
            }
        },
    },
} );
View Script Modules: The Interactivity API uses ES modules. Use viewScriptModule instead of viewScript in block.json.

Conditional Script Loading

Only load scripts when block is present:

// In PHP - already handled by viewScript
// Scripts only load when block is on the page

// For custom conditional loading:
function theme_enqueue_block_assets() {
    if ( has_block( 'theme/testimonial' ) ) {
        wp_enqueue_script( 'theme-carousel', /* ... */ );
    }
}
add_action( 'wp_enqueue_scripts', 'theme_enqueue_block_assets' );

Next Steps

In the final lesson, we'll learn how to package and distribute your blocks.

๐ŸŽฏ Lesson Complete! You can now add powerful frontend interactivity to your blocks.