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(); + } + } ); + } } );