Skip to content

Commit

Permalink
Allow passing optional afterLoad callbacks to store calls (#53363)
Browse files Browse the repository at this point in the history
* Add `afterLoad` callbacks

* Add tests for `afterLoad` callbacks

* Add changelog

* Update changelog link

* Add docs for `afterLoad`

* Move store options to the end
  • Loading branch information
DAreRodz authored Aug 11, 2023
1 parent d5d8533 commit 98e1373
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 4 deletions.
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

- 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' );
} );
} );

0 comments on commit 98e1373

Please sign in to comment.