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

Allow passing optional afterLoad callbacks to store calls #53363

Merged
merged 6 commits into from
Aug 11, 2023
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,14 @@
{
"apiVersion": 2,
"name": "test/store-afterload",
"title": "E2E Interactivity tests - store afterload",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScript": "store-afterload-view",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* HTML for testing `afterLoad` callbacks added to the store.
*
* @package gutenberg-test-interactive-blocks
*/

?>
<div data-wp-interactive>
<h3>Store statuses</h3>
<p data-store-status data-wp-text="state.status1">waiting</p>
<p data-store-status data-wp-text="state.status2">waiting</p>
<p data-store-status data-wp-text="state.status3">waiting</p>
<p data-store-status data-wp-text="state.status4">waiting</p>

<h3><code>afterLoad</code> executions</h3>
<p>All stores ready:&#20;
<span
data-testid="all-stores-ready"
data-wp-text="state.allStoresReady">
>waiting</span>
</p>
<p>vDOM ready:&#20;
<span
data-testid="vdom-ready"
data-wp-text="state.vdomReady">
>waiting</span>
</p>
<p><code>afterLoad</code> exec times:&#20;
<span
data-testid="after-load-exec-times"
data-wp-text="state.execTimes.afterLoad">
>0</span>
</p>
<p><code>sharedAfterLoad</code> exec times:&#20;
<span
data-testid="shared-after-load-exec-times"
data-wp-text="state.execTimes.sharedAfterLoad">
>0</span>
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
( ( { wp } ) => {
/**
* WordPress dependencies
*/
const { store } = wp.interactivity;

const afterLoad = ({ state }) => {
// Check the state is correctly initialized.
const { status1, status2, status3, status4 } = state;
state.allStoresReady =
[ status1, status2, status3, status4 ]
.every( ( t ) => t === 'ready' )
.toString();

// Check the HTML has been processed as well.
const selector = '[data-store-status]';
state.vdomReady =
document.querySelector( selector ) &&
Array.from(
document.querySelectorAll( selector )
).every( ( el ) => el.textContent === 'ready' ).toString();

// Increment exec times everytime this function runs.
state.execTimes.afterLoad += 1;
}

const sharedAfterLoad = ({ state }) => {
// Increment exec times everytime this function runs.
state.execTimes.sharedAfterLoad += 1;
}

// Case 1: without afterload callback
store( {
state: { status1: 'ready' },
} );

// Case 2: non-shared afterload callback
store( {
state: {
status2: 'ready',
allStoresReady: false,
vdomReady: false,
execTimes: { afterLoad: 0 },
},
}, { afterLoad } );

// Case 3: shared afterload callback
store( {
state: {
status3: 'ready',
execTimes: { sharedAfterLoad: 0 },
},
}, { afterLoad: sharedAfterLoad } );
store( {
state: {
status4: 'ready',
execTimes: { sharedAfterLoad: 0 },
},
}, { afterLoad: sharedAfterLoad } );
} )( window );
4 changes: 4 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features
luisherranz marked this conversation as resolved.
Show resolved Hide resolved

- Allow passing optional `afterLoad` callbacks to `store` calls. ([#53363](https://github.com/WordPress/gutenberg/pull/53363))

### Bug Fix

- Add support for underscores and leading dashes in the suffix part of the directive. ([#53337](https://github.com/WordPress/gutenberg/pull/53337))
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 @@ -657,5 +657,31 @@ wp_store(
);
```

### Store options

The `store` function accepts an object as a second argument with the following optional properties:

#### `afterLoad`

Callback to be executed after the Interactivity API has been set up and the store is ready. It receives the global store as argument.

```js
// view.js
store(
{
state: {
cart: [],
},
},
{
afterLoad: async ( { state } ) => {
// Let's consider `clientId` is added
// during server-side rendering.
state.cart = await getCartData( state.clientId );
},
}
);
```



2 changes: 2 additions & 0 deletions packages/interactivity/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import registerDirectives from './directives';
import { init } from './hydration';
import { rawStore, afterLoads } from './store';
export { store } from './store';
export { directive } from './hooks';
export { h as createElement } from 'preact';
Expand All @@ -16,4 +17,5 @@ registerDirectives();

document.addEventListener( 'DOMContentLoaded', async () => {
await init();
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
} );
24 changes: 20 additions & 4 deletions packages/interactivity/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,25 @@ const getSerializedState = () => {
return {};
};

export const afterLoads = new Set();

const rawState = getSerializedState();
export const rawStore = { state: deepSignal( rawState ) };

/**
* @typedef StoreProps Properties object passed to `store`.
* @property {Object} state State to be added to the global store. All the
* properties included here become reactive.
*/

/**
* @typedef StoreOptions Options object.
* @property {(store:any) => void} [afterLoad] Callback to be executed after the
* Interactivity API has been set up
* and the store is ready. It
* receives the store as argument.
*/

/**
* Extends the Interactivity API global store with the passed properties.
*
Expand Down Expand Up @@ -76,11 +92,11 @@ export const rawStore = { state: deepSignal( rawState ) };
* </div>
* ```
*
* @param {Object} properties Properties to be added to the global store.
* @param {Object} [properties.state] State to be added to the global store. All
* the properties included here become reactive.
* @param {StoreProps} properties Properties to be added to the global store.
* @param {StoreOptions} [options] Options passed to the `store` call.
*/
export const store = ( { state, ...block } ) => {
export const store = ( { state, ...block }, { afterLoad } = {} ) => {
deepMerge( rawStore, block );
deepMerge( rawState, state );
if ( afterLoad ) afterLoads.add( afterLoad );
};
40 changes: 40 additions & 0 deletions test/e2e/specs/interactivity/store-afterload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Internal dependencies
*/
import { test, expect } from './fixtures';

test.describe( 'store afterLoad callbacks', () => {
test.beforeAll( async ( { interactivityUtils: utils } ) => {
await utils.activatePlugins();
await utils.addPostWithBlock( 'test/store-afterload' );
} );

test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
await page.goto( utils.getLink( 'test/store-afterload' ) );
} );

test.afterAll( async ( { interactivityUtils: utils } ) => {
await utils.deactivatePlugins();
await utils.deleteAllPosts();
} );

test( 'run after the vdom and store are ready', async ( { page } ) => {
const allStoresReady = page.getByTestId( 'all-stores-ready' );
const vdomReady = page.getByTestId( 'vdom-ready' );

await expect( allStoresReady ).toHaveText( 'true' );
await expect( vdomReady ).toHaveText( 'true' );
} );

test( 'run once even if shared between several store calls', async ( {
page,
} ) => {
const afterLoadTimes = page.getByTestId( 'after-load-exec-times' );
const sharedAfterLoadTimes = page.getByTestId(
'shared-after-load-exec-times'
);

await expect( afterLoadTimes ).toHaveText( '1' );
await expect( sharedAfterLoadTimes ).toHaveText( '1' );
} );
} );
Loading