Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Interactive Block — Client-Side Navigation

> **Note**
> Check the [Interactivity API Reference docs in the Block Editor handbook](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/) to learn more about the Interactivity API.

This block has been created with the `create-block-interactive-template` (`client-side-navigation` variant) and demonstrates client-side navigation powered by `@wordpress/interactivity-router`.

## How it works

- **Server-rendered content** — `render.php` reads a `?quote=` query param and picks a quote from a hardcoded array, rendered inside a router region.
- **Client-side navigation** — Prev/next links change the query param and trigger navigation via `@wordpress/interactivity-router` instead of a full page reload.
- **State persistence proof** — A client-side stopwatch runs outside the navigated region, demonstrating that client state is preserved across navigations.
- **Loading indicator** — Visual feedback is shown while navigation is in progress.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';

/**
* The edit function describes the structure of your block in the context of the
* editor. This represents what the editor will render when the block is used.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
*
* @return {Element} Element to render.
*/
export default function Edit() {
const blockProps = useBlockProps();

return (
<div { ...blockProps }>
<p>
{ __(
'{{title}} — Client-Side Navigation block. Preview this block on the frontend to see the interactive quote navigator.',
'{{textdomain}}'
) }
</p>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* The following styles get applied inside the editor only.
*
* Replace them with your own styles or remove the file completely.
*/

.wp-block-{{namespace}}-{{slug}} p {
font-size: 0.9em;
color: #757575;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block
*/
import { registerBlockType } from '@wordpress/blocks';

/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* All files containing `style` keyword are bundled together. The code used
* gets applied both to the front of your site and to the editor. All other files
* get applied to the editor only.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './style.scss';
import './editor.scss';

/**
* Internal dependencies
*/
import Edit from './edit';
import metadata from './block.json';

/**
* Every block starts by registering a new block type definition.
*
* @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block
*/
registerBlockType( metadata.name, {
/**
* @see ./edit.js
*/
edit: Edit,
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* PHP file to use when rendering the block type on the server to show on the front end.
*
* The following variables are exposed to the file:
* $attributes (array): The block attributes.
* $content (string): The block default content.
* $block (WP_Block): The block instance.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/

$quotes = array(
array(
'text' => __( 'The best way to predict the future is to invent it.', '{{textdomain}}' ),
'author' => 'Alan Kay',
),
array(
'text' => __( 'Talk is cheap. Show me the code.', '{{textdomain}}' ),
'author' => 'Linus Torvalds',
),
array(
'text' => __( 'Simplicity is the soul of efficiency.', '{{textdomain}}' ),
'author' => 'Austin Freeman',
),
array(
'text' => __( 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', '{{textdomain}}' ),
'author' => 'Martin Fowler',
),
array(
'text' => __( 'First, solve the problem. Then, write the code.', '{{textdomain}}' ),
'author' => 'John Johnson',
),
);

$total = count( $quotes );
$quote_index = isset( $_GET['quote'] ) ? absint( $_GET['quote'] ) % $total : 0;
$quote = $quotes[ $quote_index ];
$prev_index = ( $quote_index - 1 + $total ) % $total;
$next_index = ( $quote_index + 1 ) % $total;

// Adds the global state.
wp_interactivity_state(
'{{namespace}}',
array(
'timer' => '00:00.00',
'isNavigating' => false,
'artificialDelay' => false,
)
);
?>

<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="{{namespace}}"
>
<div class="wp-block-{{namespace}}-{{slug}}__timer" data-wp-init="callbacks.startTimer">
<?php esc_html_e( 'Client timer:', '{{textdomain}}' ); ?>
<span data-wp-text="state.timer"></span>
</div>

<label class="wp-block-{{namespace}}-{{slug}}__delay">
<input
type="checkbox"
data-wp-bind--checked="state.artificialDelay"
data-wp-on--change="actions.toggleDelay"
/>
<?php esc_html_e( 'Introduce artificial network delay', '{{textdomain}}' ); ?>
</label>

<div
class="wp-block-{{namespace}}-{{slug}}__content"
data-wp-interactive="{{namespace}}"
data-wp-router-region="{{namespace}}/quote"
>
<figure
class="wp-block-{{namespace}}-{{slug}}__quote"
data-wp-key="quote-<?php echo esc_attr( $quote_index ); ?>"
>
<blockquote>
<p><?php echo esc_html( $quote['text'] ); ?></p>
</blockquote>
<figcaption>
— <?php echo esc_html( $quote['author'] ); ?>
</figcaption>
</figure>

<nav class="wp-block-{{namespace}}-{{slug}}__nav">
<a
href="<?php echo esc_url( add_query_arg( 'quote', $prev_index ) ); ?>"
data-wp-on--click="actions.navigateTo"
data-wp-on--mouseenter="actions.prefetchTo"
>
← <?php esc_html_e( 'Prev', '{{textdomain}}' ); ?>
</a>
<a
href="<?php echo esc_url( add_query_arg( 'quote', $next_index ) ); ?>"
data-wp-on--click="actions.navigateTo"
data-wp-on--mouseenter="actions.prefetchTo"
>
<?php esc_html_e( 'Next', '{{textdomain}}' ); ?> →
</a>
</nav>

<div
class="wp-block-{{namespace}}-{{slug}}__loading"
data-wp-bind--hidden="!state.isNavigating"
>
<?php esc_html_e( 'Loading…', '{{textdomain}}' ); ?>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* The following styles get applied both on the front of your site
* and in the editor.
*
* Replace them with your own styles or remove the file completely.
*/

.wp-block-{{namespace}}-{{slug}} {
font-family: sans-serif;
max-width: 480px;
padding: 1.5em;
border: 1px solid #e0e0e0;
border-radius: 8px;

// Timer — sits outside the router region.
&__timer {
font-size: 0.85em;
color: #666;
padding-bottom: 1em;
margin-bottom: 1em;
border-bottom: 1px solid #eee;
font-variant-numeric: tabular-nums;

span {
font-weight: 600;
}
}

// Quote card.
&__quote {
margin: 0 0 1.5em;

blockquote {
margin: 0;
font-size: 1.15em;
line-height: 1.6;
}

figcaption {
margin-top: 0.75em;
font-size: 0.9em;
color: #555;
}
}

// Prev / Next navigation.
&__nav {
display: flex;
justify-content: space-between;
gap: 1em;

a {
padding: 0.4em 1em;
border: 1px solid #ccc;
border-radius: 4px;
text-decoration: none;
color: inherit;
font-size: 0.9em;

&:hover {
background: #f5f5f5;
}
}
}

// Artificial delay checkbox.
&__delay {
display: flex;
align-items: center;
gap: 0.5em;
margin-bottom: 1em;
font-size: 0.85em;
color: #666;
cursor: pointer;
}

// Router region content wrapper.
&__content {
position: relative;
}

// Loading overlay — appears on top of the quote after 100ms.
&__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
font-size: 0.9em;
color: #666;
animation: wp-block-create-block-loading-fade-in 0s 100ms both;

&[hidden] {
display: none;
}
}
}

@keyframes wp-block-create-block-loading-fade-in {
from {
opacity: 0;
}

to {
opacity: 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* WordPress dependencies
*/
import { getElement, store, withSyncEvent } from '@wordpress/interactivity';

const { state } = store( '{{namespace}}', {
state: {
// Elapsed time in centiseconds, used to derive the formatted timer.
elapsed: 0,
get timer() {
const cs = state.elapsed % 100;
const s = Math.floor( state.elapsed / 100 ) % 60;
const m = Math.floor( state.elapsed / 6000 );
const pad = ( n, len = 2 ) => String( n ).padStart( len, '0' );
return `${ pad( m ) }:${ pad( s ) }.${ pad( cs ) }`;
},
},
actions: {
/**
* Navigates to the clicked link using the Interactivity Router.
* The router swaps only the router-region content — no full
* page reload.
*/
navigateTo: withSyncEvent( function* ( event ) {
event.preventDefault();
// getElement() must be called synchronously, before any yield.
const { attributes } = getElement();
state.isNavigating = true;
if ( state.artificialDelay ) {
yield new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
}
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.navigate( attributes.href );
state.isNavigating = false;
} ),
*prefetchTo() {
const { attributes } = getElement();
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.prefetch( attributes.href );
},
toggleDelay() {
state.artificialDelay = ! state.artificialDelay;
},
},
callbacks: {
/**
* Starts a stopwatch that ticks every 10 ms.
* Because it lives outside the router region, the timer state
* persists across client-side navigations — proving that no
* full page reload happened.
*/
startTimer() {
const interval = setInterval( () => {
state.elapsed += 1;
}, 10 );
return () => clearInterval( interval );
},
},
} );
14 changes: 14 additions & 0 deletions packages/create-block-interactive-template/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ module.exports = {
'An interactive block with the Interactivity API using TypeScript.',
viewScriptModule: 'file:./view.ts',
},
'client-side-navigation': {
slug: 'example-interactive-client-side-navigation',
title: 'Example Interactive Client-Side Navigation',
description:
'An interactive block demonstrating client-side navigation with the Interactivity API Router.',
blockTemplatesPath: join(
__dirname,
'block-templates-client-side-navigation'
),
npmDependencies: [
'@wordpress/interactivity',
'@wordpress/interactivity-router',
],
},
},
pluginTemplatesPath: join( __dirname, 'plugin-templates' ),
blockTemplatesPath: join( __dirname, 'block-templates' ),
Expand Down
Loading