diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php
index 47eb351d837e7..bfac62feb1359 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php
@@ -260,3 +260,54 @@
data-wp-text="context.callbackRunCount"
>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js
index 6ceef82864d9d..7577810b6bb87 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js
@@ -13,6 +13,28 @@ const { state } = store( 'directive-each' );
store( 'directive-each', {
state: {
letters: [ 'A', 'B', 'C' ],
+ eachUndefined: undefined,
+ eachNull: null,
+ eachArray: [ 'an', 'array' ],
+ eachSet: new Set( [ 'a', 'set' ] ),
+ eachString: 'str',
+ *eachGenerator() {
+ yield 'a';
+ yield 'generator';
+ },
+ eachIterator: {
+ [ Symbol.iterator ]() {
+ const vals = [ 'implements', 'iterator' ];
+ let i = 0;
+ return {
+ next() {
+ return i < vals.length
+ ? { value: vals[ i++ ], done: false }
+ : { done: true };
+ },
+ };
+ },
+ },
},
} );
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index 6b32e4ec35a97..ec54cdfd7c036 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+### Enhancements
+
+- Allow more iterables to be used in each directives ([#67798](https://github.com/WordPress/gutenberg/pull/67798)).
+
+### Bug Fixes
+
+- Fix an error when the value used in an each directive is not iterable ([#67798](https://github.com/WordPress/gutenberg/pull/67798)).
+
## 6.14.0 (2024-12-11)
## 6.13.0 (2024-11-27)
diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx
index 31e07d095e0a4..bddd017b1c99d 100644
--- a/packages/interactivity/src/directives.tsx
+++ b/packages/interactivity/src/directives.tsx
@@ -4,7 +4,7 @@
/**
* External dependencies
*/
-import { h as createElement, type RefObject } from 'preact';
+import { h as createElement, type VNode, type RefObject } from 'preact';
import { useContext, useMemo, useRef } from 'preact/hooks';
/**
@@ -567,11 +567,19 @@ export default () => {
const [ entry ] = each;
const { namespace } = entry;
- const list = evaluate( entry );
+ const iterable = evaluate( entry );
+
+ if ( typeof iterable?.[ Symbol.iterator ] !== 'function' ) {
+ return;
+ }
+
const itemProp = isNonDefaultDirectiveSuffix( entry )
? kebabToCamelCase( entry.suffix )
: 'item';
- return list.map( ( item ) => {
+
+ const result: VNode< any >[] = [];
+
+ for ( const item of iterable ) {
const itemContext = proxifyContext(
proxifyState( namespace, {} ),
inheritedValue.client[ namespace ]
@@ -596,12 +604,15 @@ export default () => {
? getEvaluate( { scope } )( eachKey[ 0 ] )
: item;
- return createElement(
- Provider,
- { value: mergedContext, key },
- element.props.content
+ result.push(
+ createElement(
+ Provider,
+ { value: mergedContext, key },
+ element.props.content
+ )
);
- } );
+ }
+ return result;
},
{ priority: 20 }
);
diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx
index e9b9f48ba3518..7899e3eafd228 100644
--- a/packages/interactivity/src/hooks.tsx
+++ b/packages/interactivity/src/hooks.tsx
@@ -77,7 +77,7 @@ interface DirectiveArgs {
}
export interface DirectiveCallback {
- ( args: DirectiveArgs ): VNode< any > | null | void;
+ ( args: DirectiveArgs ): VNode< any > | VNode< any >[] | null | void;
}
interface DirectiveOptions {
diff --git a/test/e2e/specs/interactivity/directive-each.spec.ts b/test/e2e/specs/interactivity/directive-each.spec.ts
index 511b38e7ddbb8..3c015e63fe4bc 100644
--- a/test/e2e/specs/interactivity/directive-each.spec.ts
+++ b/test/e2e/specs/interactivity/directive-each.spec.ts
@@ -18,7 +18,7 @@ test.describe( 'data-wp-each', () => {
await utils.deleteAllPosts();
} );
- test( 'should use `item` as the defaul item name in the context', async ( {
+ test( 'should use `item` as the default item name in the context', async ( {
page,
} ) => {
const elements = page.getByTestId( 'letters' ).getByTestId( 'item' );
@@ -500,4 +500,37 @@ test.describe( 'data-wp-each', () => {
await expect( element ).toHaveText( 'beta' );
await expect( callbackRunCount ).toHaveText( '1' );
} );
+
+ for ( const testId of [
+ 'each-with-unset',
+ 'each-with-null',
+ 'each-with-undefined',
+ ] ) {
+ test( `does not error with non-iterable values: ${ testId }`, async ( {
+ page,
+ } ) => {
+ await expect( page.getByTestId( testId ) ).toBeEmpty();
+ } );
+ }
+
+ for ( const [ testId, values ] of [
+ [ 'each-with-array', [ 'an', 'array' ] ],
+ [ 'each-with-set', [ 'a', 'set' ] ],
+ [ 'each-with-string', [ 's', 't', 'r' ] ],
+ [ 'each-with-generator', [ 'a', 'generator' ] ],
+
+ // TODO: Is there a problem with proxies here?
+ // [ 'each-with-iterator', [ 'implements', 'iterator' ] ],
+ ] as const ) {
+ test( `support different each iterable values: ${ testId }`, async ( {
+ page,
+ } ) => {
+ const element = page.getByTestId( testId );
+ for ( const value of values ) {
+ await expect(
+ element.getByText( value, { exact: true } )
+ ).toBeVisible();
+ }
+ } );
+ }
} );