Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: router with region-based client-side navigation #53733

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,16 @@
</button>
</div>
</div>

<div
data-wp-interactive
data-wp-navigation-id="navigation"
data-wp-context='{ "text": "first page" }'
>
<div data-testid="navigation text" data-wp-text="context.text"></div>
<div data-testid="navigation new text" data-wp-text="context.newText"></div>
<button data-testid="toggle text" data-wp-on--click="actions.toggleText">Toggle Text</button>
<button data-testid="add new text" data-wp-on--click="actions.addNewText">Add New Text</button>
<button data-testid="navigate" data-wp-on--click="actions.navigate">Navigate</button>
<button data-testid="async navigate" data-wp-on--click="actions.asyncNavigate">Async Navigate</button>
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
( ( { wp } ) => {
const { store } = wp.interactivity;
const { store, navigate } = wp.interactivity;

const html = `
<div
data-wp-interactive
data-wp-navigation-id="navigation"
data-wp-context='{ "text": "second page" }'
>
<div data-testid="navigation text" data-wp-text="context.text"></div>
<div data-testid="navigation new text" data-wp-text="context.newText"></div>
<button data-testid="toggle text" data-wp-on--click="actions.toggleText">Toggle Text</button>
<button data-testid="add new text" data-wp-on--click="actions.addNewText">Add new text</button>
<button data-testid="navigate" data-wp-on--click="actions.navigate">Navigate</button>
<button data-testid="async navigate" data-wp-on--click="actions.asyncNavigate">Async Navigate</button>
</div>`;

store( {
derived: {
Expand All @@ -17,6 +31,25 @@
toggleContextText: ( { context } ) => {
context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1';
},
toggleText: ( { context } ) => {
context.text = "changed dynamically";
},
addNewText: ( { context } ) => {
context.newText = 'some new text';
},
navigate: () => {
navigate( window.location, {
force: true,
html,
} );
},
asyncNavigate: async ({ context }) => {
await navigate( window.location, {
force: true,
html,
} );
context.newText = 'changed from async action';
}
},
} );
} )( window );
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"apiVersion": 2,
"name": "test/directive-key",
"title": "E2E Interactivity tests - directive key",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScript": "directive-key-view",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/**
* HTML for testing the directive `data-wp-key`.
*
* @package gutenberg-test-interactive-blocks
*/

?>

<div data-wp-interactive data-wp-navigation-id="some-id">
<ul>
<li data-wp-key="id-2" data-testid="first-item">2</li>
<li data-wp-key="id-3">3</li>
</ul>
<button data-testid="navigate" data-wp-on--click="actions.navigate">
Navigate
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
( ( { wp } ) => {
const { store, navigate } = wp.interactivity;

const html = `
<div data-wp-interactive data-wp-navigation-id="some-id">
<ul>
<li data-wp-key="id-1">1</li>
<li data-wp-key="id-2" data-testid="second-item">2</li>
<li data-wp-key="id-3">3</li>
</ul>
</div>`;

store( {
actions: {
navigate: () => {
navigate( window.location, {
force: true,
html,
} );
},
},
} );
} )( window );
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"apiVersion": 2,
"name": "test/router-regions",
"title": "E2E Interactivity tests - router regions",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScript": "router-regions-view",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
/**
* HTML for testing the hydration of router regions.
*
* @package gutenberg-test-interactive-blocks
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
*/

?>

<section>
<h2>Region 1</h2>
<div data-wp-interactive data-wp-navigation-id="region-1">
<p
data-testid="region-1-text"
data-wp-text="state.region1.text"
>not hydrated</p>
<p
data-testid="region-1-ssr"
>content from page <?php echo $attributes['page']; ?></p>

<button
data-testid="state-counter"
data-wp-text="state.counter.value"
data-wp-on--click="actions.counter.increment"
>NaN</button>

<?php if ( isset( $attributes['next'] ) ) : ?>
<a
data-testid="next"
data-wp-on--click="actions.router.navigate"
href="<?php echo $attributes['next']; ?>"
>Next</a>
<?php else : ?>
<a
data-testid="back"
data-wp-on--click="actions.router.back"
href="#"
>Back</a>
<?php endif; ?>
</div>
</section>

<div>
<p
data-testid="no-region-text-1"
data-wp-text="state.region1.text"
>not hydrated</p>
</div>


<section>
<h2>Region 2</h2>
<div data-wp-interactive data-wp-navigation-id="region-2">
<p
data-testid="region-2-text"
data-wp-text="state.region2.text"
>not hydrated</p>
<p
data-testid="region-2-ssr"
>content from page <?php echo $attributes['page']; ?></p>

<button
data-testid="context-counter"
data-wp-context='{ "counter": { "initialValue": 0 } }'
data-wp-init="actions.counter.init"
data-wp-text="context.counter.value"
data-wp-on--click="actions.counter.increment"
>NaN</button>

<div data-wp-ignore>
<div>
<p
data-testid="no-region-text-2"
data-wp-text="state.region2.text"
>not hydrated</p>
</div>

<section>
<h2>Nested region</h2>
<div data-wp-interactive data-wp-navigation-id="nested-region">
<p
data-testid="nested-region-ssr"
>content from page <?php echo $attributes['page']; ?></p>
</div>
</section>
</div>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
( ( { wp } ) => {
/**
* WordPress dependencies
*/
const { store, navigate } = wp.interactivity;

store( {
state: {
region1: {
text: 'hydrated'
},
region2: {
text: 'hydrated'
},
counter: {
value: 0,
},
},
actions: {
router: {
navigate: async ( { event: e } ) => {
e.preventDefault();
await navigate( e.target.href );
},
back: () => history.back(),
},
counter: {
increment: ( { state, context } ) => {
if ( context.counter ) {
context.counter.value += 1;
} else {
state.counter.value += 1;
}
},
init: ( { context } ) => {
if ( context.counter ) {
context.counter.value = context.counter.initialValue;
}
}
},
},
} );
} )( window );
6 changes: 6 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Enhancements

- Support keys using `data-wp-key`. ([#53844](https://github.com/WordPress/gutenberg/pull/53844))
- Merge new server-side rendered context on client-side navigation. ([#53853](https://github.com/WordPress/gutenberg/pull/53853))
- Support region-based client-side navigation. ([#53733](https://github.com/WordPress/gutenberg/pull/53733))

## 2.1.0 (2023-08-16)

### New Features
Expand Down
26 changes: 26 additions & 0 deletions packages/interactivity/docs/2-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ DOM elements are connected to data stored in the state & context through directi
- [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg)
- [`wp-effect`](#wp-effect) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg)
- [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg)
- [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg)
- [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties)
- [The store](#the-store)
- [Elements of the store](#elements-of-the-store)
Expand Down Expand Up @@ -449,6 +450,31 @@ store( {

The `wp-init` can return a function. If it does, the returned function will run when the element is removed from the DOM.

#### `wp-key`


The `wp-key` directive assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements. This becomes important if your array elements can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key value helps the Interactivity API infer what exactly has changed in the array, allowing it to make the correct updates to the DOM.

The key should be a string that uniquely identifies the element among its siblings. Typically it is used on repeated elements like list items. For example:

```html
<ul>
<li data-wp-key="unique-id-1">Item 1</li>
<li data-wp-key="unique-id-2">Item 2</li>
</ul>
```

But it can also be used on other elements:

```html
<div>
<a data-wp-key="previous-page" ...>Previous page</a>
<a data-wp-key="next-page" ...>Next page</a>
</div>
```

When the list is re-rendered, the Interactivity API will match elements by their keys to determine if an item was added/removed/reordered. Elements without keys might be recreated unnecessarily.

### Values of directives are references to store properties

The value assigned to a directive is a string pointing to a specific state, selector, action, or effect. *Using a Namespace is highly recommended* to define these elements of the store.
Expand Down
40 changes: 21 additions & 19 deletions packages/interactivity/src/directives.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { useContext, useMemo, useEffect } from 'preact/hooks';
import { useContext, useMemo, useEffect, useRef } from 'preact/hooks';
import { deepSignal, peek } from 'deepsignal';

/**
Expand All @@ -14,18 +14,16 @@ import { directive } from './hooks';
const isObject = ( item ) =>
item && typeof item === 'object' && ! Array.isArray( item );

const mergeDeepSignals = ( target, source ) => {
const mergeDeepSignals = ( target, source, overwrite ) => {
for ( const k in source ) {
if ( typeof peek( target, k ) === 'undefined' ) {
target[ `$${ k }` ] = source[ `$${ k }` ];
} else if (
isObject( peek( target, k ) ) &&
isObject( peek( source, k ) )
) {
if ( isObject( peek( target, k ) ) && isObject( peek( source, k ) ) ) {
mergeDeepSignals(
target[ `$${ k }` ].peek(),
source[ `$${ k }` ].peek()
source[ `$${ k }` ].peek(),
overwrite
);
} else if ( overwrite || typeof peek( target, k ) === 'undefined' ) {
target[ `$${ k }` ] = source[ `$${ k }` ];
}
}
};
Expand All @@ -36,20 +34,24 @@ export default () => {
'context',
( {
directives: {
context: { default: context },
context: { default: newContext },
},
props: { children },
context: inherited,
context: inheritedContext,
} ) => {
const { Provider } = inherited;
const inheritedValue = useContext( inherited );
const value = useMemo( () => {
const localValue = deepSignal( context );
mergeDeepSignals( localValue, inheritedValue );
return localValue;
}, [ context, inheritedValue ] );
const { Provider } = inheritedContext;
const inheritedValue = useContext( inheritedContext );
const currentValue = useRef( deepSignal( {} ) );
currentValue.current = useMemo( () => {
const newValue = deepSignal( newContext );
mergeDeepSignals( newValue, inheritedValue );
mergeDeepSignals( currentValue.current, newValue, true );
return currentValue.current;
}, [ newContext, inheritedValue ] );

return <Provider value={ value }>{ children }</Provider>;
return (
<Provider value={ currentValue.current }>{ children }</Provider>
);
},
{ priority: 5 }
);
Expand Down
Loading
Loading