Product Collection: Trigger `wc-blocks_product_list_rendered` JS event (#50166)
* Dispatch JS event about PC being rendered * Revert format changes * Write the callback * Add functions descriptions * Add changelog * Remove empty line * Add tests * Rename test cases * Replace waiting for page load with more reliable expect.poll * Remove leftover step Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com> * Fix typo in function name * Add collection name to default Product Collection block * Expect collection name in the event * Expose the collection name through IAPI context * Send the collection name with the event * Trigger event also on page change * Remove unused CUSTOM collection type * Provide documentation * Update TOC * Update tests that verify the event payload * Improve E2E tests further * Don't add a Product Catalog collection type to default collection * Avoid repeating the same piece of code by extracting some function on tag processor * Rename function to better depict its purpose * Move the documentation to the right place * Remove the unused variable * Add example to dom-events doc * Update documentation * Update docs manifest * Attach default collection name * Add the default collection context in PHP so it covers all the cases * Prevent exposing product catalog collection name in event * Update docs * Update test --------- Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com>
This commit is contained in:
parent
7802209887
commit
8bdc78c777
|
@ -1007,9 +1007,18 @@
|
|||
"menu_title": "Registering custom collections",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/register-product-collection.md",
|
||||
"hash": "88445929a9f76512e1e8ff60be7beff7e912f31fbad552abf18862ed85f00585",
|
||||
"hash": "e3df65c5eec52e4bb797e34c040dbb8f820ea6571e9ce50b1d518e95ca6cb169",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/register-product-collection.md",
|
||||
"id": "3bf26fc7c56ae6e6a56e1171f750f5204fcfcece"
|
||||
},
|
||||
{
|
||||
"post_title": "DOM Events sent from product collection block",
|
||||
"menu_title": "DOM Events",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md",
|
||||
"hash": "78bce4ab5b5e902232b5ff73fd7a7c197e4f4417a490ccb45c9a27400d003787",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md",
|
||||
"id": "c8d247b91472740075871e6b57a9583d893ac650"
|
||||
}
|
||||
],
|
||||
"categories": []
|
||||
|
@ -1697,5 +1706,5 @@
|
|||
"categories": []
|
||||
}
|
||||
],
|
||||
"hash": "c24136612fe16fc7dee435a2ad2198ce242ab63dbba483fd0a18efd88dc3a289"
|
||||
"hash": "945d1eb884a52c8c0dbc4c8852760e332ab40030de82403d1a7103b2517c36a1"
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
post_title: DOM Events sent from product collection block
|
||||
menu_title: DOM Events
|
||||
tags: how-to
|
||||
---
|
||||
|
||||
# Product Collection - DOM Events
|
||||
|
||||
## `wc-blocks_product_list_rendered`
|
||||
|
||||
This event is triggered when Product Collection block was rendered or re-rendered (e.g. due to page change).
|
||||
|
||||
### `detail` parameters
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||
|
||||
### Example usage
|
||||
|
||||
```javascript
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
( e ) => {
|
||||
const { collection } = e.detail;
|
||||
console.log( collection ) // -> collection name, e.g. woocommerce/product-collection/on-sale
|
||||
}
|
||||
);
|
||||
```
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import type { AddToCartEventDetail } from '@woocommerce/types';
|
||||
import type { CoreCollectionNames } from '@woocommerce/blocks/product-collection/types';
|
||||
|
||||
const CustomEvent = window.CustomEvent || null;
|
||||
|
||||
|
@ -59,6 +60,16 @@ export const triggerAddedToCartEvent = ( {
|
|||
} );
|
||||
};
|
||||
|
||||
export const triggerProductListRenderedEvent = ( payload: {
|
||||
collection?: CoreCollectionNames | string;
|
||||
} ) => {
|
||||
dispatchEvent( 'wc-blocks_product_list_rendered', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that listens to a jQuery event and dispatches a native JS event.
|
||||
* Useful to convert WC Core events into events that can be read by blocks.
|
||||
|
|
|
@ -8,10 +8,12 @@ import {
|
|||
getElement,
|
||||
getContext,
|
||||
} from '@woocommerce/interactivity';
|
||||
import { triggerProductListRenderedEvent } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CoreCollectionNames } from './types';
|
||||
import './style.scss';
|
||||
|
||||
export type ProductCollectionStoreContext = {
|
||||
|
@ -20,6 +22,7 @@ export type ProductCollectionStoreContext = {
|
|||
accessibilityMessage: string;
|
||||
accessibilityLoadingMessage: string;
|
||||
accessibilityLoadedMessage: string;
|
||||
collection: CoreCollectionNames;
|
||||
};
|
||||
|
||||
const isValidLink = ( ref: HTMLAnchorElement ) =>
|
||||
|
@ -136,6 +139,10 @@ const productCollectionStore = {
|
|||
ctx.isPrefetchNextOrPreviousLink = !! ref.href;
|
||||
|
||||
scrollToFirstProductIfNotVisible( wcNavigationId );
|
||||
|
||||
triggerProductListRenderedEvent( {
|
||||
collection: ctx.collection,
|
||||
} );
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
@ -179,6 +186,12 @@ const productCollectionStore = {
|
|||
yield prefetch( ref.href );
|
||||
}
|
||||
},
|
||||
*onRender() {
|
||||
const { collection } =
|
||||
getContext< ProductCollectionStoreContext >();
|
||||
|
||||
triggerProductListRenderedEvent( { collection } );
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -135,7 +135,6 @@ export type QueryControlProps = {
|
|||
|
||||
export enum CoreCollectionNames {
|
||||
PRODUCT_CATALOG = 'woocommerce/product-collection/product-catalog',
|
||||
CUSTOM = 'woocommerce/product-collection/custom',
|
||||
BEST_SELLERS = 'woocommerce/product-collection/best-sellers',
|
||||
FEATURED = 'woocommerce/product-collection/featured',
|
||||
NEW_ARRIVALS = 'woocommerce/product-collection/new-arrivals',
|
||||
|
|
|
@ -18,13 +18,13 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ProductCollectionAttributes,
|
||||
TProductCollectionOrder,
|
||||
TProductCollectionOrderBy,
|
||||
ProductCollectionQuery,
|
||||
ProductCollectionDisplayLayout,
|
||||
PreviewState,
|
||||
SetPreviewState,
|
||||
type ProductCollectionAttributes,
|
||||
type TProductCollectionOrder,
|
||||
type TProductCollectionOrderBy,
|
||||
type ProductCollectionQuery,
|
||||
type ProductCollectionDisplayLayout,
|
||||
type PreviewState,
|
||||
type SetPreviewState,
|
||||
} from './types';
|
||||
import {
|
||||
coreQueryPaginationBlockName,
|
||||
|
|
|
@ -1627,6 +1627,72 @@ test.describe( 'Product Collection', () => {
|
|||
await expect( products ).toHaveText( expectedProducts );
|
||||
} );
|
||||
} );
|
||||
|
||||
test.describe( 'Extensibility - JS events', () => {
|
||||
test( 'emits wc-blocks_product_list_rendered event on init and on page change', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
( e ) => {
|
||||
const { collection } = e.detail;
|
||||
window.eventPayload = collection;
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => await page.evaluate( 'window.eventPayload' )
|
||||
)
|
||||
.toBe( undefined );
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 1 );
|
||||
|
||||
await page.getByRole( 'link', { name: 'Next Page' } ).click();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 2 );
|
||||
} );
|
||||
|
||||
test( 'emits one wc-blocks_product_list_rendered event per block', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
// Adding three blocks in total
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
() => {
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 3 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Product Collection: emit the JS event when PC block is rendered
|
|
@ -156,56 +156,102 @@ class ProductCollection extends AbstractBlock {
|
|||
}
|
||||
|
||||
/**
|
||||
* Enhances the Product Collection block with client-side pagination.
|
||||
* Check if next tag is a PC block.
|
||||
*
|
||||
* This function identifies Product Collection blocks and adds necessary data attributes
|
||||
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
|
||||
* @param WP_HTML_Tag_processor $p Initial tag processor.
|
||||
*
|
||||
* @return bool Answer if PC block is available.
|
||||
*/
|
||||
private function is_next_tag_product_collection( $p ) {
|
||||
return $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set PC block namespace for Interactivity API.
|
||||
*
|
||||
* @param WP_HTML_Tag_processor $p Initial tag processor.
|
||||
*/
|
||||
private function set_product_collection_namespace( $p ) {
|
||||
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the init directive to Product Collection block to call
|
||||
* the onRender callback.
|
||||
*
|
||||
* @param string $block_content The HTML content of the block.
|
||||
* @param array $block Block details, including its attributes.
|
||||
* @param string $collection Collection type.
|
||||
*
|
||||
* @return string Updated block content with added interactivity attributes.
|
||||
* @return string Updated HTML content.
|
||||
*/
|
||||
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
|
||||
if ( $is_product_collection_block && $is_enhanced_pagination_enabled ) {
|
||||
// Enqueue the Interactivity API runtime.
|
||||
wp_enqueue_script( 'wc-interactivity' );
|
||||
private function add_rendering_callback( $block_content, $collection ) {
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
|
||||
// Add `data-wc-navigation-id to the product collection block.
|
||||
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) ) ) {
|
||||
$p->set_attribute(
|
||||
'data-wc-navigation-id',
|
||||
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
|
||||
);
|
||||
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
|
||||
// Add `data-init to the product collection block so we trigger JS event on render.
|
||||
if ( $this->is_next_tag_product_collection( $p ) ) {
|
||||
$p->set_attribute(
|
||||
'data-wc-init',
|
||||
'callbacks.onRender'
|
||||
);
|
||||
if ( $collection ) {
|
||||
$p->set_attribute(
|
||||
'data-wc-context',
|
||||
wp_json_encode(
|
||||
array(
|
||||
// The message to be announced by the screen reader when the page is loading or loaded.
|
||||
'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
|
||||
'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
|
||||
// We don't prefetch the links if user haven't clicked on pagination links yet.
|
||||
// This way we avoid prefetching when the page loads.
|
||||
'isPrefetchNextOrPreviousLink' => false,
|
||||
'collection' => $collection,
|
||||
),
|
||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
|
||||
)
|
||||
);
|
||||
$block_content = $p->get_updated_html();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two div's:
|
||||
* 1. Pagination animation for visual users.
|
||||
* 2. Accessibility div for screen readers, to announce page load states.
|
||||
*/
|
||||
$last_tag_position = strripos( $block_content, '</div>' );
|
||||
$accessibility_and_animation_html = '
|
||||
return $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach all the Interactivity API directives responsible
|
||||
* for client-side navigation.
|
||||
*
|
||||
* @param string $block_content The HTML content of the block.
|
||||
*
|
||||
* @return string Updated HTML content.
|
||||
*/
|
||||
private function enable_client_side_navigation( $block_content ) {
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
|
||||
// Add `data-wc-navigation-id to the product collection block.
|
||||
if ( $this->is_next_tag_product_collection( $p ) ) {
|
||||
$p->set_attribute(
|
||||
'data-wc-navigation-id',
|
||||
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
|
||||
);
|
||||
$current_context = json_decode( $p->get_attribute( 'data-wc-context' ), true ) ?? [];
|
||||
$p->set_attribute(
|
||||
'data-wc-context',
|
||||
wp_json_encode(
|
||||
array(
|
||||
...$current_context,
|
||||
// The message to be announced by the screen reader when the page is loading or loaded.
|
||||
'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
|
||||
'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
|
||||
// We don't prefetch the links if user haven't clicked on pagination links yet.
|
||||
// This way we avoid prefetching when the page loads.
|
||||
'isPrefetchNextOrPreviousLink' => false,
|
||||
),
|
||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
|
||||
)
|
||||
);
|
||||
$block_content = $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two div's:
|
||||
* 1. Pagination animation for visual users.
|
||||
* 2. Accessibility div for screen readers, to announce page load states.
|
||||
*/
|
||||
$last_tag_position = strripos( $block_content, '</div>' );
|
||||
$accessibility_and_animation_html = '
|
||||
<div
|
||||
data-wc-interactive="{"namespace":"woocommerce/product-collection"}"
|
||||
class="wc-block-product-collection__pagination-animation"
|
||||
|
@ -219,12 +265,44 @@ class ProductCollection extends AbstractBlock {
|
|||
data-wc-text="context.accessibilityMessage">
|
||||
</div>
|
||||
';
|
||||
$block_content = substr_replace(
|
||||
$block_content,
|
||||
$accessibility_and_animation_html,
|
||||
$last_tag_position,
|
||||
0
|
||||
);
|
||||
return substr_replace(
|
||||
$block_content,
|
||||
$accessibility_and_animation_html,
|
||||
$last_tag_position,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the Product Collection block with client-side pagination.
|
||||
*
|
||||
* This function identifies Product Collection blocks and adds necessary data attributes
|
||||
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
|
||||
*
|
||||
* @param string $block_content The HTML content of the block.
|
||||
* @param array $block Block details, including its attributes.
|
||||
*
|
||||
* @return string Updated block content with added interactivity attributes.
|
||||
*/
|
||||
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
|
||||
if ( $is_product_collection_block ) {
|
||||
// Enqueue the Interactivity API runtime and set the namespace.
|
||||
wp_enqueue_script( 'wc-interactivity' );
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
if ( $this->is_next_tag_product_collection( $p ) ) {
|
||||
$this->set_product_collection_namespace( $p );
|
||||
}
|
||||
$block_content = $p->get_updated_html();
|
||||
|
||||
$collection = $block['attrs']['collection'] ?? '';
|
||||
$block_content = $this->add_rendering_callback( $block_content, $collection );
|
||||
|
||||
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
|
||||
if ( $is_enhanced_pagination_enabled ) {
|
||||
$block_content = $this->enable_client_side_navigation( $block_content );
|
||||
}
|
||||
}
|
||||
|
||||
return $block_content;
|
||||
|
@ -295,7 +373,7 @@ class ProductCollection extends AbstractBlock {
|
|||
'class_name' => $class_name,
|
||||
)
|
||||
) ) {
|
||||
$processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
|
||||
$this->set_product_collection_namespace( $processor );
|
||||
$processor->set_attribute( 'data-wc-on--click', 'actions.navigate' );
|
||||
$processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) );
|
||||
|
||||
|
@ -314,9 +392,11 @@ class ProductCollection extends AbstractBlock {
|
|||
*/
|
||||
private function is_block_compatible( $block_name ) {
|
||||
// Check for explicitly unsupported blocks.
|
||||
if ( 'core/post-content' === $block_name ||
|
||||
if (
|
||||
'core/post-content' === $block_name ||
|
||||
'woocommerce/mini-cart' === $block_name ||
|
||||
'woocommerce/featured-product' === $block_name ) {
|
||||
'woocommerce/featured-product' === $block_name
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -835,7 +915,7 @@ class ProductCollection extends AbstractBlock {
|
|||
if ( ! isset( $base[ $key ] ) ) {
|
||||
$base[ $key ] = array();
|
||||
}
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
} else {
|
||||
$base[ $key ] = $value;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue