Skip to content

Commit

Permalink
Interactivity API: Prevent each directive errors and allow more itera…
Browse files Browse the repository at this point in the history
…bles (WordPress#67798)

Fix an issue where the each directive would error when a non-iterable value is received.
Support more iterable values in the each directive.

---

Co-authored-by: sirreal <jonsurrell@git.wordpress.org>
Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: luisherranz <luisherranz@git.wordpress.org>
  • Loading branch information
4 people authored Dec 16, 2024
1 parent 52b0db4 commit f5b8bec
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,54 @@
data-wp-text="context.callbackRunCount"
></data>
</div>

<hr>

<div
data-wp-interactive="directive-each"
data-testid="each-with-unset"
>
<template data-wp-each="state.eachUnset"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-null"
>
<template data-wp-each="state.eachNull"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-undefined"
>
<template data-wp-each="state.eachUndefined"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-array"
>
<template data-wp-each="state.eachArray"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-set"
>
<template data-wp-each="state.eachSet"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-string"
>
<template data-wp-each="state.eachString"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-generator"
>
<template data-wp-each="state.eachGenerator"><p data-wp-text="context.item"></p></template>
</div>
<div
data-wp-interactive="directive-each"
data-testid="each-with-iterator"
>
<template data-wp-each="state.eachIterator"><p data-wp-text="context.item"></p></template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
};
},
},
},
} );

Expand Down
8 changes: 8 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 19 additions & 8 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 ]
Expand All @@ -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 }
);
Expand Down
2 changes: 1 addition & 1 deletion packages/interactivity/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 34 additions & 1 deletion test/e2e/specs/interactivity/directive-each.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -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();
}
} );
}
} );

0 comments on commit f5b8bec

Please sign in to comment.