How to enable and use theInteractivity API on your projects
In the ever-evolving world of web development, creating interactive features within WordPress blocks has been streamlined thanks to the Interactivity API. This API introduces an abstraction layer similar to popular JavaScript frameworks, making it easier to attach common events, such as clicks, with minimal custom code. As a result, developers can now build more efficient interactive blocks while maintaining a more manageable codebase.
The Interactivity API has several noteworthy features:
- Custom Stores: Simplify creating custom stores using the store function.
- Simplified Bindings: Reduce boilerplate code for binding data and actions.
- Interactivity Directives: Facilitate adding interactivity to blocks declaratively.
- Modular Approach: Promotes a clean separation of concerns and code reusability.
- Enhanced Developer Experience: Streamlined integration and improved error handling.
However, some configuration is necessary to leverage the Interactivity API’s full potential and its scripts. Given that @wordpress/interactivity
provides its scripts as a JS module, we can only import and use a module script within another module script. Below are the steps required to add interactivity to custom and core blocks.
Discussing details on various features like creating the store, types of directives available, and accessing different stores are beyond the scope of this article. Please refer to the provided links at the end of the article to learn more about the interactivity API.
How to add Interactivity API to Custom Blocks
Interactivity API requires at least WordPress 6.5. The following guide is for adding an interactive block in a plugin that was NOT created using the wordpress/create-block tool and the scaffolding template create-block-interactive-template.
A newly created plugin with these tools will have all the latest dependencies. So, you can directly run the block scaffold command mentioned in step 4.
This section can be referred to while adding Interactivity API to the blocks that are created by us. i.e. Custom Blocks.
Creating a custom interactive block using the Interactivity API involves several steps. Follow this guide to get started:
1. Update @wordpress/scripts
to version 27.6.0.
2. Add @wordpress/interactivity
as a dependency.
...
"dependencies": {
"@wordpress/interactivity": "^5.4.0"
},
"devDependencies": {
...
"@wordpress/scripts": "27.6.0",
...
},
...
3. Add --experimental-modules
to build and start wp-scripts
command in package.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/",
}
4. Create a new block using the @wordpress/create-block
command 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.
Files and configurations
The following files and configurations are added to the block:
1. block.json
– Contains the block configuration.
- interactivity
supports
is set totrue
.
{
...
"supports": {
"interactivity": true
}
...
}
2. 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>
3. 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}`);
},
},
});
Directives
wp-interactivity
- Activates interactivity for the element and its children. The
create-block
is the namespace for the store.
- Activates interactivity for the element and its children. The
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
You can test out the code directly by cloning this features-plugin-skeleton branch.
Output
Next, let’s apply this to a real-world use case.
How to add Interactivity API to Core Blocks
This section can be referred to while adding Interactivity API to the blocks that are not in our control. i.e. Core Blocks and Third-party Blocks.
Imagine wanting to create a core/button that plays a core/video. Using the Interactivity API, you can add interactivity directives to the core blocks and add custom scripts to make them interactive relative to one another.
To use the Interactivity API with core blocks, you need to follow these steps:
1. Update @wordpress/scripts
to version 27.6.0.
2. Add @wordpress/interactivity
as a dependency.
...
"dependencies": {
"@wordpress/interactivity": "^5.4.0"
},
"devDependencies": {
...
"@wordpress/scripts": "27.6.0",
...
},
...
3. Add --experimental-modules
to build and start wp-scripts
command in package.json
.
"scripts": {
"build:js": "wp-scripts build --experimental-modules --config ./webpack.config.js",
"start:js": "wp-scripts start --experimental-modules --config ./webpack.config.js",
}
4. Update webpack configuration to enable module output.
Import getAsBooleanFromENV
and use it to get the WP_EXPERIMENTAL_MODULES
env variable to check if --experimental-modules
flag is set.
/**
* WordPress dependencies
*/
const { getAsBooleanFromENV } = require( '@wordpress/scripts/utils' );
// Check if the --experimental-modules flag is set.
const hasExperimentalModulesFlag = getAsBooleanFromENV( 'WP_EXPERIMENTAL_MODULES' );
If --experimental-modules
flag is set then import both scriptConfig and moduleConfig else import the scriptConfig
.
let scriptConfig, moduleConfig;
if ( hasExperimentalModulesFlag ) {
[ scriptConfig, moduleConfig ] = require( '@wordpress/scripts/config/webpack.config' );
} else {
scriptConfig = require( '@wordpress/scripts/config/webpack.config' );
}
Replace the defaultConfig
with scriptConfig inside sharedConfig
object.
// 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() ] ),
},
};
Check if --experimental-module
flag is set and create a moduleScripts
config using moduleConfig
to enable module output for the scripts to the assets/build/js/modules
folder.
// 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',
},
};
}
Combine the script, style, and module script configs and export them.
const customExports = [ scripts, styles ];
if ( hasExperimentalModulesFlag ) {
customExports.push( moduleScripts );
}
module.exports = customExports;
Finally, the file will look like this if you have started from the theme-elementary webpack.config.js
/**
* 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;
5. Add interactivity directives to the core block markup by checking the classes of the blocks. You can add it inside inc/classes/block-extensions/class-media-text-interactive.php
.
/**
* Render block core/button.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
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();
}
add_filter( 'render_block_core/button', 'render_block_core_button', 10, 2 );
/**
* Render block core/columns.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
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();
}
add_filter( 'render_block_core/columns', 'render_block_core_columns', 10, 2 );
/**
* Render block core/video.
*
* @param string $block_content Block content.
* @param array $block Block.
* @return string
*/
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();
}
add_filter( 'render_block_core/video', 'render_block_core_video', 10, 2 );
6. Create a module script to add interactivity to the core blocks. In theme-elementary
you can add it inside assets/src/js/modules/core-video.js
.
/**
* 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;
}
},
},
} );
You can test out the code directly by cloning this theme-elementary branch.
7. Add the class elementary-media-text-interactive
to button
, video
and columns
.
Output
References and further reading
On this page
Leave a Reply