diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js
index 29997853322..5a00d60534e 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js
@@ -15,3 +15,4 @@ import './product-elements/stock-indicator';
import './product-elements/add-to-cart';
import './product-elements/product-image-gallery';
import './product-elements/product-details';
+import './product-elements/related-products';
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json
new file mode 100644
index 00000000000..e6e3a2a9aaf
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json
@@ -0,0 +1,17 @@
+{
+ "name": "woocommerce/related-products",
+ "version": "1.0.0",
+ "title": "Related Products",
+ "icon": "product",
+ "description": "Display related products.",
+ "category": "woocommerce",
+ "supports": {
+ "align": true,
+ "reusable": false
+ },
+ "keywords": [ "WooCommerce" ],
+ "usesContext": [ "postId", "postType", "queryId" ],
+ "textdomain": "woo-gutenberg-products-block",
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx
new file mode 100644
index 00000000000..36778c17b1f
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import {
+ BLOCK_ATTRIBUTES,
+ INNER_BLOCKS_TEMPLATE,
+} from '@woocommerce/blocks/product-query/variations';
+import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
+import { InnerBlockTemplate } from '@wordpress/blocks';
+import { Disabled, Notice } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import './editor.scss';
+
+const Edit = () => {
+ const TEMPLATE: InnerBlockTemplate[] = [
+ [ 'core/query', BLOCK_ATTRIBUTES, INNER_BLOCKS_TEMPLATE ],
+ ];
+ const blockProps = useBlockProps();
+
+ return (
+
+
+
+
+ { __(
+ 'These products will vary depending on the main product in the page',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+
+
+
+ );
+};
+
+export default Edit;
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss
new file mode 100644
index 00000000000..b3872daea3a
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss
@@ -0,0 +1,4 @@
+.wc-block-editor-related-products__notice {
+ margin: 10px auto;
+ max-width: max-content;
+}
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx
new file mode 100644
index 00000000000..1db84adc949
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+import { box as icon } from '@wordpress/icons';
+import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
+import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import save from './save';
+import metadata from './block.json';
+
+registerBlockSingleProductTemplate( {
+ registerBlockFn: () => {
+ // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core.
+ registerBlockType( metadata, {
+ icon,
+ edit,
+ save,
+ } );
+ },
+ unregisterBlockFn: () => {
+ unregisterBlockType( metadata.name );
+ },
+ blockName: metadata.name,
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx
new file mode 100644
index 00000000000..0feb6d8f950
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx
@@ -0,0 +1,17 @@
+/**
+ * External dependencies
+ */
+import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
+
+const Save = () => {
+ const blockProps = useBlockProps.save();
+
+ return (
+
+ { /* @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core*/ }
+
+
+ );
+};
+
+export default Save;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx
index 312ce2af40d..04425a80c2e 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx
@@ -14,6 +14,7 @@ import { CORE_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-
import './inspector-controls';
import './style.scss';
import './variations/product-query';
+import './variations/related-products';
const EXTENDED_CORE_ELEMENTS = [
PRODUCT_SUMMARY_ID,
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts
index e8bc6128918..cbee9c48cc8 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts
@@ -112,4 +112,5 @@ export interface ProductQueryContext {
export enum QueryVariation {
/** The main, fully customizable, Product Query block */
PRODUCT_QUERY = 'woocommerce/product-query',
+ RELATED_PRODUCTS = 'woocommerce/related-products',
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts
new file mode 100644
index 00000000000..9dcdfe4481e
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts
@@ -0,0 +1 @@
+export * from './related-products';
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx
new file mode 100644
index 00000000000..4286baeef79
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+import {
+ InnerBlockTemplate,
+ registerBlockVariation,
+ unregisterBlockVariation,
+} from '@wordpress/blocks';
+import { Icon } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { stacks } from '@woocommerce/icons';
+import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
+
+/**
+ * Internal dependencies
+ */
+import { QUERY_LOOP_ID } from '../constants';
+
+import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './elements/product-template';
+import { VARIATION_NAME as PRODUCT_TITLE_ID } from './elements/product-title';
+
+const VARIATION_NAME = 'woocommerce/related-products';
+
+export const BLOCK_ATTRIBUTES = {
+ namespace: VARIATION_NAME,
+ allowedControls: [],
+ displayLayout: {
+ type: 'flex',
+ columns: 5,
+ },
+ query: {
+ perPage: 5,
+ pages: 0,
+ offset: 0,
+ postType: 'product',
+ order: 'asc',
+ orderBy: 'title',
+ author: '',
+ search: '',
+ exclude: [],
+ sticky: '',
+ inherit: false,
+ },
+ lock: {
+ remove: true,
+ move: true,
+ },
+};
+
+export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
+ [
+ 'core/post-template',
+ { __woocommerceNamespace: PRODUCT_TEMPLATE_ID },
+ [
+ [
+ 'woocommerce/product-image',
+ {
+ productId: 0,
+ },
+ ],
+ [
+ 'core/post-title',
+ {
+ textAlign: 'center',
+ level: 3,
+ fontSize: 'medium',
+ __woocommerceNamespace: PRODUCT_TITLE_ID,
+ },
+ [],
+ ],
+ [
+ 'woocommerce/product-price',
+ {
+ textAlign: 'center',
+ fontSize: 'small',
+ style: {
+ spacing: {
+ margin: { bottom: '1rem' },
+ },
+ },
+ },
+ [],
+ ],
+ [
+ 'woocommerce/product-button',
+ {
+ textAlign: 'center',
+ fontSize: 'small',
+ style: {
+ spacing: {
+ margin: { bottom: '1rem' },
+ },
+ },
+ },
+ [],
+ ],
+ ],
+ ],
+];
+
+registerBlockSingleProductTemplate( {
+ registerBlockFn: () =>
+ registerBlockVariation( QUERY_LOOP_ID, {
+ description: __(
+ 'Display related products.',
+ 'woo-gutenberg-products-block'
+ ),
+ name: 'Related Products Controls',
+ title: __(
+ 'Related Products Controls',
+ 'woo-gutenberg-products-block'
+ ),
+ isActive: ( blockAttributes ) =>
+ blockAttributes.namespace === VARIATION_NAME,
+ icon: (
+
+ ),
+ attributes: BLOCK_ATTRIBUTES,
+ // Gutenberg doesn't support this type yet, discussion here:
+ // https://github.com/WordPress/gutenberg/pull/43632
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ allowedControls: [],
+ innerBlocks: INNER_BLOCKS_TEMPLATE,
+ scope: [ 'block' ],
+ } ),
+ unregisterBlockFn: () =>
+ unregisterBlockVariation( QUERY_LOOP_ID, 'Related Products' ),
+ blockName: VARIATION_NAME,
+} );
diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php
index 85805715cfd..2eae4a8cdf5 100644
--- a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php
+++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php
@@ -81,7 +81,7 @@ class ProductQuery extends AbstractBlock {
* @param array $parsed_block The block being rendered.
* @return boolean
*/
- private function is_woocommerce_variation( $parsed_block ) {
+ public static function is_woocommerce_variation( $parsed_block ) {
return isset( $parsed_block['attrs']['namespace'] )
&& substr( $parsed_block['attrs']['namespace'], 0, 11 ) === 'woocommerce';
}
@@ -99,7 +99,7 @@ class ProductQuery extends AbstractBlock {
$this->parsed_block = $parsed_block;
- if ( $this->is_woocommerce_variation( $parsed_block ) ) {
+ if ( self::is_woocommerce_variation( $parsed_block ) ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
diff --git a/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php b/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php
new file mode 100644
index 00000000000..e77416edf92
--- /dev/null
+++ b/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php
@@ -0,0 +1,89 @@
+parsed_block = $parsed_block;
+
+ if ( ProductQuery::is_woocommerce_variation( $parsed_block ) && 'woocommerce/related-products' === $parsed_block['attrs']['namespace'] ) {
+ // Set this so that our product filters can detect if it's a PHP template.
+ add_filter(
+ 'query_loop_block_query_vars',
+ array( $this, 'build_query' ),
+ 10,
+ 1
+ );
+ }
+ }
+
+
+
+ /**
+ * Return a custom query based on attributes, filters and global WP_Query.
+ *
+ * @param WP_Query $query The WordPress Query.
+ * @return array
+ */
+ public function build_query( $query ) {
+ $parsed_block = $this->parsed_block;
+ if ( ! ProductQuery::is_woocommerce_variation( $parsed_block ) && 'woocommerce/related-products' !== $parsed_block['attrs']['namespace'] ) {
+ return $query;
+ }
+ $product = wc_get_product();
+ $related_products = wc_get_related_products( $product->get_id() );
+
+ return array(
+ 'post_type' => 'product',
+ 'post__in' => $related_products,
+ 'post_status' => 'publish',
+ );
+ }
+
+
+}
diff --git a/plugins/woocommerce-blocks/src/BlockTypesController.php b/plugins/woocommerce-blocks/src/BlockTypesController.php
index 2b46c98831e..5ad32e5558a 100644
--- a/plugins/woocommerce-blocks/src/BlockTypesController.php
+++ b/plugins/woocommerce-blocks/src/BlockTypesController.php
@@ -212,6 +212,7 @@ final class BlockTypesController {
'RatingFilter',
'ReviewsByCategory',
'ReviewsByProduct',
+ 'RelatedProducts',
'ProductDetails',
'StockFilter',
];