Skip to content

Commit

Permalink
Interactivity API: Add getServerState() and getServerContext() (#…
Browse files Browse the repository at this point in the history
…65151)

* Expose client and server context from provider

* Create `getServerContext`

* Add simple test for server context

* Implement `getServerState`

* Add tests for read-only state proxies

* Add e2e tests for `getServerState()`

* Avoid PHPCS UndefinedVariable error

* Add e2e tests for `getServerContext` WIP

* Finish e2e tests for `getServerContext()`

* Update `getServerState()` tests

* Revert "Add simple test for server context"

This reverts commit 7e6f530.

* Update TSDocs

Co-authored-by: DAreRodz <[email protected]>
Co-authored-by: luisherranz <[email protected]>
Co-authored-by: sethrubenstein <[email protected]>
Co-authored-by: michalczaplinski <[email protected]>
  • Loading branch information
5 people authored Sep 19, 2024
1 parent eb715c2 commit 001241b
Show file tree
Hide file tree
Showing 18 changed files with 858 additions and 25 deletions.
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

0 comments on commit 001241b

Please sign in to comment.