Interactivity API is a new API that allows developers to create interactive blocks in WordPress. It is built on top of the Block Editor and provides a way to create blocks that can interact with each other. Before the Interactivity API, developers had to rely on custom JavaScript code to create interactive blocks.
Some configurations are required to use the Interactivity API and its scripts. As @wordpress/intereactivity
provides its scripts as js module and we can only import use a module script in another module script.
To create a custom interactive block using the Interactivity API, you need to follow these steps:
- Update
@wordpress/scripts
to version 27.6.0. - Add
@wordpress/interactivity
as a dependency. - Add
--experimental-modules
to the build and startwp-scripts
command inpackage.json
.
"scripts": {
"build:blocks": "wp-scripts build --experimental-modules --config ./node_modules/@wordpress/scripts/config/webpack.config.js --webpack-src-dir=./assets/src/blocks/ --output-path=./assets/build/blocks/",
}
- Create a new block using the
@wordpress/create-block
command from inside the blocks folder.
npx @wordpress/create-block@latest example-block-interactive --template @wordpress/create-block-interactive-template --no-plugin
This will scaffold a new block with the necessary files and configurations to get started with the Interactivity API.
The following files and configurations are added to the block:
-
block.json
- Contains the block configuration.- interactivity supports is set to
true
. -
{ ... "supports": { "interactivity": true } ... }
- interactivity supports is set to
-
render.php
- Contains the block's server-side rendering logic.-
<?php $unique_id = wp_unique_id( 'p-' ); ?> <div <?php echo get_block_wrapper_attributes(); ?> data-wp-interactive="create-block" <?php echo wp_interactivity_data_wp_context( array( 'isOpen' => false ) ); ?> data-wp-watch="callbacks.logIsOpen" > <button data-wp-on--click="actions.toggle" data-wp-bind--aria-expanded="context.isOpen" aria-controls="<?php echo esc_attr( $unique_id ); ?>" > <?php esc_html_e( 'Toggle', 'example-block-interactive' ); ?> </button> <p id="<?php echo esc_attr( $unique_id ); ?>" data-wp-bind--hidden="!context.isOpen" > <?php esc_html_e( 'Example Block Interactive - hello from an interactive block!', 'example-block-interactive' ); ?> </p> </div>
-
-
view.js
- Contains the block's client-side script.-
/** * WordPress dependencies */ import { store, getContext } from '@wordpress/interactivity'; store( 'create-block', { actions: { toggle: () => { const context = getContext(); context.isOpen = ! context.isOpen; }, }, callbacks: { logIsOpen: () => { const { isOpen } = getContext(); // Log the value of `isOpen` each time it changes. console.log( `Is open: ${ isOpen }` ); }, }, } );
-
wp-interactivity
- Here
data-wp-interactivity="create-block"
is used to activate the interactivity for the element and its children. Thecreate-block
is the namespace for the store.
- Here
wp-context
- Here
<?php echo wp_interactivity_data_wp_context( array( 'isOpen' => false ) ); ?>
is used to set the initial context for the block. - Here the
isOpen
state is set tofalse
initially. - It'll look like this
data-wp-context="{'isOpen':false}"
- Here
wp-watch
- Here
data-wp-watch="callbacks.logIsOpen"
is used to watch theisOpen
state and log it to the console. - It runs a callback when the node is created and runs it again when the state or context used in the callback changes.
- Here
wp-on
- Here
data-wp-on--click="actions.toggle"
is used to bind thetoggle
action to theclick
event of the button.
- Here
wp-bind
- Here
data-wp-bind--aria-expanded="context.isOpen"
is used to bind thearia-expanded
attribute of the button to theisOpen
state. - So, when the
isOpen
state istrue
, thearia-expanded
attribute will be added to the button. - Here
data-wp-bind--hidden="!context.isOpen"
is used to bind thehidden
attribute of the paragraph to the negation of theisOpen
state. - So, when the
isOpen
state isfalse
, thehidden
attribute will be added to the paragraph.
- Here
Screen.Recording.2024-04-16.at.9.03.18.PM.mov
If we want to create a core/button that plays a core/video. We can use the Interactivity API to add interactivity directives to the core blocks. And add custom script to make them interactive.
To use the Interactivity API with core blocks, you need to follow these steps:
- Update
@wordpress/scripts
to version 27.6.0. - Add
@wordpress/interactivity
as a dependency. - Add
--experimental-modules
to the build and startwp-scripts
command inpackage.json
.
"scripts": {
"build:js": "wp-scripts build --experimental-modules --config ./webpack.config.js",
"start:js": "wp-scripts start --experimental-modules --config ./webpack.config.js",
}
- Update webpack configuration to enable module output.
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' );
const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' );
/**
* WordPress dependencies
*/
const { getAsBooleanFromENV } = require( '@wordpress/scripts/utils' );
// Check if the --experimental-modules flag is set.
const hasExperimentalModulesFlag = getAsBooleanFromENV( 'WP_EXPERIMENTAL_MODULES' );
let scriptConfig, moduleConfig;
if ( hasExperimentalModulesFlag ) {
[ scriptConfig, moduleConfig ] = require( '@wordpress/scripts/config/webpack.config' );
} else {
scriptConfig = require( '@wordpress/scripts/config/webpack.config' );
}
// Extend the default config.
const sharedConfig = {
...scriptConfig,
output: {
path: path.resolve( process.cwd(), 'assets', 'build', 'js' ),
filename: '[name].js',
chunkFilename: '[name].js',
},
plugins: [
...scriptConfig.plugins
.map(
( plugin ) => {
if ( plugin.constructor.name === 'MiniCssExtractPlugin' ) {
plugin.options.filename = '../css/[name].css';
}
return plugin;
},
),
new RemoveEmptyScriptsPlugin(),
],
optimization: {
...scriptConfig.optimization,
splitChunks: {
...scriptConfig.optimization.splitChunks,
},
minimizer: scriptConfig.optimization.minimizer.concat( [ new CssMinimizerPlugin() ] ),
},
};
const styles = {
...sharedConfig,
entry: () => {
const entries = {};
const dir = './assets/src/css';
if ( ! fs.existsSync( dir ) ) {
return entries;
}
if ( fs.readdirSync( dir ).length === 0 ) {
return entries;
}
fs.readdirSync( dir ).forEach( ( fileName ) => {
const fullPath = `${ dir }/${ fileName }`;
if ( ! fs.lstatSync( fullPath ).isDirectory() ) {
entries[ fileName.replace( /\.[^/.]+$/, '' ) ] = fullPath;
}
} );
return entries;
},
module: {
...sharedConfig.module,
},
plugins: [
...sharedConfig.plugins.filter(
( plugin ) => plugin.constructor.name !== 'DependencyExtractionWebpackPlugin',
),
],
};
const scripts = {
...sharedConfig,
entry: {
accordion: path.resolve( process.cwd(), 'assets', 'src', 'js', 'accordion.js' ),
video: path.resolve( process.cwd(), 'assets', 'src', 'js', 'video.js' ),
},
};
// Add module scripts configuration if the --experimental-modules flag is set.
let moduleScripts = {};
if ( hasExperimentalModulesFlag ) {
moduleScripts = {
...moduleConfig,
entry: {
'core-video': path.resolve( process.cwd(), 'assets', 'src', 'js', 'modules', 'core-video.js' ),
},
output: {
...moduleConfig.output,
path: path.resolve( process.cwd(), 'assets', 'build', 'js', 'modules' ),
filename: '[name].js',
chunkFilename: '[name].js',
},
};
}
const customExports = [ scripts, styles ];
if ( hasExperimentalModulesFlag ) {
customExports.push( moduleScripts );
}
module.exports = customExports;
- This configuration will enable module output for the scripts present in the
assets/src/js/modules
folder.
- Create a block extensions class to add interactivity directives to the core blocks by checking the classes of the blocks.
<?php
/**
* Media Text Interactive.
*
* @package Elementary-Theme
*/
namespace Elementary_Theme\Block_Extensions;
use WP_HTML_Tag_Processor;
use Elementary_Theme\Traits\Singleton;
/**
* Class Media_Text_Interactive
*/
class Media_Text_Interactive {
use Singleton;
/**
* Constructor.
*/
protected function __construct() {
$this->setup_hooks();
}
/**
* Setup hooks.
*
* @return void
*/
public function setup_hooks() {
add_filter( 'render_block_core/button', array( $this, 'render_block_core_button' ), 10, 2 );
add_filter( 'render_block_core/columns', array( $this, 'render_block_core_columns' ), 10, 2 );
add_filter( 'render_block_core/video', array( $this, 'render_block_core_video' ), 10, 2 );
}
/**
* Render block core/button.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
public function render_block_core_button( $block_content, $block ) {
if ( ! isset( $block['attrs']['className'] ) || ! str_contains( $block['attrs']['className'], 'elementary-media-text-interactive' ) ) {
return $block_content;
}
$p = new WP_HTML_Tag_Processor( $block_content );
$p->next_tag();
$p->set_attribute( 'data-wp-on--click', 'actions.play' );
return $p->get_updated_html();
}
/**
* Render block core/columns.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
public function render_block_core_columns( $block_content, $block ) {
if ( ! isset( $block['attrs']['className'] ) || ! str_contains( $block['attrs']['className'], 'elementary-media-text-interactive' ) ) {
return $block_content;
}
wp_enqueue_script_module(
'@elementary/media-text-interactive',
sprintf( '%s/js/modules/media-text-interactive.js', ELEMENTARY_THEME_BUILD_URI ),
[
'@wordpress/interactivity',
]
); // enqueue the module script using the `wp_enqueue_script_module` function.
$p = new WP_HTML_Tag_Processor( $block_content );
$p->next_tag();
$p->set_attribute( 'data-wp-interactive', '{ "namespace": "elementary/media-text-interactive" }' );
$p->set_attribute( 'data-wp-context', '{ "isPlaying": false }' );
return $p->get_updated_html();
}
/**
* Render block core/video.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
public function render_block_core_video( $block_content, $block ) {
if ( ! isset( $block['attrs']['className'] ) || ! str_contains( $block['attrs']['className'], 'elementary-media-text-interactive' ) ) {
return $block_content;
}
$p = new WP_HTML_Tag_Processor( $block_content );
$p->next_tag();
$p->set_attribute( 'data-wp-watch', 'callbacks.playVideo' );
return $p->get_updated_html();
}
}
- Create a module script to add interactivity directives to the core blocks.
/**
* Custom module script required for the media text interactive pattern.
*/
/**
* WordPress dependencies
*/
import { store, getContext, getElement } from '@wordpress/interactivity';
store( 'elementary/media-text-interactive', {
actions: {
/**
* Update the video play state.
*
* @return {void}
*/
play() {
const context = getContext();
context.isPlaying = true;
},
},
callbacks: {
/**
* Play the video.
*
* @return {void}
*/
playVideo() {
const context = getContext();
const { ref } = getElement();
if ( context.isPlaying ) {
ref.querySelector( 'video' )?.play();
context.isPlaying = false;
}
},
},
} );
Screen.Recording.2024-04-16.at.9.16.51.PM.mov
- You can test out the code on the following branches:
- Interactivity API dev note
- Quick start guide
- Script Modules
- Interactivity API with core blocks
- API Reference
- Demo
- Demo Source
- Future Features