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: Add getServerState() and getServerContext() #65151

Merged
merged 12 commits into from
Sep 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ directive(
'test-context',
( { context: { Provider }, props: { children } } ) => {
executionProof( 'context' );
const value = {
const client = {
[ namespace ]: proxifyState( namespace, {
attribute: 'from context',
text: 'from context',
} ),
};
return h( Provider, { value }, children );
return h( Provider, { value: { client } }, children );
},
{ priority: 8 }
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "test/get-server-context",
"title": "E2E Interactivity tests - getServerContext",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* HTML for testing the getServerContext() function.
*
* @package gutenberg-test-interactive-blocks
*
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
*/

$link1 = $attributes['links']['modified'];
$link2 = $attributes['links']['newProps'];
$parent_ctx = $attributes['parentContext'];
$child_ctx = $attributes['childContext'];
?>

<nav
data-testid="navigate"
data-wp-interactive="test/get-server-context"
data-wp-on--click="actions.navigate"
>
<a data-testid="modified" href="<?php echo esc_url( $link1 ); ?>">modified</a>
<a data-testid="newProps" href="<?php echo esc_url( $link2 ); ?>">newProps</a>
</nav>

<div
data-wp-interactive="test/get-server-context"
data-wp-router-region="server-context"
data-wp-watch="callbacks.updateServerContextParent"
<?php echo wp_interactivity_data_wp_context( $parent_ctx ); ?>
>
<div
data-wp-watch="callbacks.updateServerContextChild"
<?php echo wp_interactivity_data_wp_context( $child_ctx ); ?>
>
<div data-testid="prop" data-wp-text="context.prop"></div>
<div data-testid="nested.prop" data-wp-text="context.nested.prop"></div>
<div data-testid="newProp" data-wp-text="context.newProp"></div>
<div data-testid="nested.newProp" data-wp-text="context.nested.newProp"></div>
<div data-testid="inherited.prop" data-wp-text="context.inherited.prop"></div>
<div data-testid="inherited.newProp" data-wp-text="context.inherited.newProp"></div>

<button
data-testid="tryToModifyServerContext"
<?php echo wp_interactivity_data_wp_context( array( 'result' => 'modify' ) ); ?>
data-wp-on--click="actions.attemptModification"
data-wp-text="context.result">
>
modify
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php return array(
'dependencies' => array(
'@wordpress/interactivity',
array(
'id' => '@wordpress/interactivity-router',
'import' => 'dynamic',
),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* WordPress dependencies
*/
import { store, getContext, getServerContext } from '@wordpress/interactivity';

store( 'test/get-server-context', {
actions: {
*navigate( e ) {
e.preventDefault();
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.navigate( e.target.href );
},
attemptModification() {
try {
getServerContext().prop = 'updated from client';
getContext().result = 'unexpectedly modified ❌';
} catch ( e ) {
getContext().result = 'not modified ✅';
}
},
},
callbacks: {
updateServerContextParent() {
const ctx = getContext();
const { prop, newProp, nested, inherited } = getServerContext();
ctx.prop = prop;
ctx.newProp = newProp;
ctx.nested.prop = nested.prop;
ctx.nested.newProp = nested.newProp;
ctx.inherited.prop = inherited.prop;
ctx.inherited.newProp = inherited.newProp;
},
updateServerContextChild() {
const ctx = getContext();
const { prop, newProp, nested, inherited } = getServerContext();
ctx.prop = prop;
ctx.newProp = newProp;
ctx.nested.prop = nested.prop;
ctx.nested.newProp = nested.newProp;
ctx.inherited.prop = inherited.prop;
ctx.inherited.newProp = inherited.newProp;
},
},
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "test/get-server-state",
"title": "E2E Interactivity tests - getServerState",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/**
* HTML for testing the getServerState() function.
*
* @package gutenberg-test-interactive-blocks
*
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
*/

if ( isset( $attributes['state'] ) ) {
wp_interactivity_state( 'test/get-server-state', $attributes['state'] );
}
?>

<div
data-wp-interactive="test/get-server-state"
data-wp-watch="callbacks.updateState"
>
<div data-testid="prop" data-wp-text="state.prop"></div>
<div data-testid="nested.prop" data-wp-text="state.nested.prop"></div>
<div data-testid="newProp" data-wp-text="state.newProp"></div>
<div data-testid="nested.newProp" data-wp-text="state.nested.newProp"></div>

<button
data-testid="tryToModifyServerState"
<?php echo wp_interactivity_data_wp_context( array( 'result' => 'modify' ) ); ?>
data-wp-on--click="actions.attemptModification"
data-wp-text="context.result">
>
modify
</button>


<nav>
<?php
if ( isset( $attributes['links'] ) ) {
foreach ( $attributes['links'] as $key => $link ) {
$i = $key += 1;
echo <<<HTML
<a
data-testid="link $i"
data-wp-on--click="actions.navigate"
href="$link"
>link $i</a>
HTML;
}
}
?>
</nav>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php return array(
'dependencies' => array(
'@wordpress/interactivity',
array(
'id' => '@wordpress/interactivity-router',
'import' => 'dynamic',
),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* WordPress dependencies
*/
import { store, getServerState, getContext } from '@wordpress/interactivity';

const { state } = store( 'test/get-server-state', {
actions: {
*navigate( e ) {
e.preventDefault();
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.navigate( e.target.href );
},
attemptModification() {
try {
getServerState().prop = 'updated from client';
getContext().result = 'unexpectedly modified ❌';
} catch ( e ) {
getContext().result = 'not modified ✅';
}
},
},
callbacks: {
updateState() {
const { prop, newProp, nested } = getServerState();
state.prop = prop;
state.newProp = newProp;
state.nested.prop = nested.prop;
state.nested.newProp = nested.newProp;
},
},
} );
43 changes: 30 additions & 13 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,19 @@ export default () => {
const defaultEntry = context.find(
( { suffix } ) => suffix === 'default'
);
const inheritedValue = useContext( inheritedContext );
const { client: inheritedClient, server: inheritedServer } =
useContext( inheritedContext );

const ns = defaultEntry!.namespace;
const currentValue = useRef( proxifyState( ns, {} ) );
const client = useRef( proxifyState( ns, {} ) );
const server = useRef( proxifyState( ns, {}, { readOnly: true } ) );

// No change should be made if `defaultEntry` does not exist.
const contextStack = useMemo( () => {
const result = { ...inheritedValue };
const result = {
client: { ...inheritedClient },
server: { ...inheritedServer },
};
if ( defaultEntry ) {
const { namespace, value } = defaultEntry;
// Check that the value is a JSON object. Send a console warning if not.
Expand All @@ -159,17 +164,22 @@ export default () => {
);
}
deepMerge(
currentValue.current,
client.current,
deepClone( value ) as object,
false
);
result[ namespace ] = proxifyContext(
currentValue.current,
inheritedValue[ namespace ]
deepMerge( server.current, deepClone( value ) as object );
result.client[ namespace ] = proxifyContext(
client.current,
inheritedClient[ namespace ]
);
result.server[ namespace ] = proxifyContext(
server.current,
inheritedServer[ namespace ]
);
}
return result;
}, [ defaultEntry, inheritedValue ] );
}, [ defaultEntry, inheritedClient, inheritedServer ] );

return createElement( Provider, { value: contextStack }, children );
},
Expand Down Expand Up @@ -563,17 +573,24 @@ export default () => {
suffix === 'default' ? 'item' : kebabToCamelCase( suffix );
const itemContext = proxifyContext(
proxifyState( namespace, {} ),
inheritedValue[ namespace ]
inheritedValue.client[ namespace ]
);
const mergedContext = {
...inheritedValue,
[ namespace ]: itemContext,
client: {
...inheritedValue.client,
[ namespace ]: itemContext,
},
server: { ...inheritedValue.server },
};

// Set the item after proxifying the context.
mergedContext[ namespace ][ itemProp ] = item;
mergedContext.client[ namespace ][ itemProp ] = item;

const scope = { ...getScope(), context: mergedContext };
const scope = {
...getScope(),
context: mergedContext.client,
serverContext: mergedContext.server,
};
const key = eachKey
? getEvaluate( { scope } )( eachKey[ 0 ] )
: item;
Expand Down
6 changes: 4 additions & 2 deletions packages/interactivity/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ interface DirectivesProps {
}

// Main context.
const context = createContext< any >( {} );
const context = createContext< any >( { client: {}, server: {} } );

// WordPress Directives.
const directiveCallbacks: Record< string, DirectiveCallback > = {};
Expand Down Expand Up @@ -253,7 +253,9 @@ const Directives = ( {
// element ref, state and props.
const scope = useRef< Scope >( {} as Scope ).current;
scope.evaluate = useCallback( getEvaluate( { scope } ), [] );
scope.context = useContext( context );
const { client, server } = useContext( context );
scope.context = client;
scope.serverContext = server;
/* eslint-disable react-hooks/rules-of-hooks */
scope.ref = previousScope?.ref || useRef( null );
/* eslint-enable react-hooks/rules-of-hooks */
Expand Down
4 changes: 2 additions & 2 deletions packages/interactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { getNamespace } from './namespaces';
import { parseServerData, populateServerData } from './store';
import { proxifyState } from './proxies';

export { store, getConfig } from './store';
export { getContext, getElement } from './scopes';
export { store, getConfig, getServerState } from './store';
export { getContext, getServerContext, getElement } from './scopes';
export {
withScope,
useWatch,
Expand Down
Loading
Loading