diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-each/block.json new file mode 100644 index 00000000000000..0d35e461056683 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/directive-each", + "title": "E2E Interactivity tests - directive each", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "directive-each-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php new file mode 100644 index 00000000000000..b9deee18abe57d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php @@ -0,0 +1,226 @@ + + +
+
+ + +

A

+

B

+

C

+
+ +
+ +
+ + + + + +

avocado

+

banana

+

cherimoya

+
+ +
+ +
+ + + + + + +

A Game of Thrones

+

A Clash of Kings

+

A Storm of Swords

+
+ +
+ +
+ + + +

1

+

2

+

3

+

4

+
+ +
+ +
+ + +

item X

+
+ +
+ + +

two

+

2

+

three

+

3

+

four

+

4

+
+ +
+ + + + +
+ +
+
+

+
+
+ + +
+ + + +

avocado

+

banana

+

cherimoya

+
+
+ +
+ +
+ + +

beta

+

gamma

+

delta

+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js new file mode 100644 index 00000000000000..55677c9629ad39 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js @@ -0,0 +1,189 @@ +/** + * WordPress dependencies + */ +import { store, getContext, navigate } from '@wordpress/interactivity'; + +const { state } = store( 'directive-each' ); + +store( 'directive-each', { + state: { + letters: [ 'A', 'B', 'C' ], + }, +} ); + +store( 'directive-each', { + state: { + fruits: [ 'avocado', 'banana', 'cherimoya' ], + get fruitId() { + const { idPrefix, fruit } = getContext(); + return `${idPrefix}${fruit}`; + } + }, + actions: { + removeFruit() { + const { fruit } = getContext(); + state.fruits.splice( state.fruits.indexOf( fruit ), 1 ); + }, + rotateFruits() { + const fruit = state.fruits.pop(); + state.fruits.splice( 0, 0, fruit ); + }, + addFruit() { + state.fruits.splice( 0, 0, 'ananas' ); + }, + replaceFruit() { + state.fruits.splice( 0, 1, 'ananas' ); + }, + }, +} ); + +store( 'directive-each', { + state: { + books: [ + { + title: 'A Game of Thrones', + author: 'George R.R. Martin', + isbn: "9780553588484", + }, + { + title: 'A Clash of Kings', + author: 'George R.R. Martin', + isbn: "9780553381696", + }, + { + title: 'A Storm of Swords', + author: 'George R.R. Martin', + isbn: "9780553573428", + }, + ], + }, + actions: { + removeBook() { + const { book } = getContext(); + state.books.splice( state.books.indexOf( book ), 1 ); + }, + rotateBooks() { + const book = state.books.pop(); + state.books.splice( 0, 0, book ); + }, + addBook() { + const book = { + title: 'A Feast for Crows', + author: 'George R.R. Martin', + isbn: "9780553582024", + }; + state.books.splice( 0, 0, book ); + }, + replaceBook() { + const book = { + title: 'A Feast for Crows', + author: 'George R.R. Martin', + isbn: "9780553582024", + }; + state.books.splice( 0, 1, book ); + }, + modifyBook() { + const [ book ] = state.books; + book.title = book.title.toUpperCase(); + }, + }, +} ); + +store( 'directive-each', { + state: { + numbers: [ 1, 2, 3 ], + }, + actions: { + shiftNumber() { + state.numbers.shift(); + }, + unshiftNumber() { + if ( state.numbers.length > 0 ) { + state.numbers.unshift( state.numbers[ 0 ] - 1 ); + } + } + }, +} ); + +store( 'directive-each', { + state: { + emptyList: [] + }, + actions: { + addItem() { + state.emptyList.push( `item ${ state.emptyList.length }` ); + } + }, +} ); + +store( 'directive-each', { + state: { + numbersAndNames: [ + { name: "two", value: 2 }, + { name: "three", value: 3 }, + ], + }, + actions: { + unshiftNumberAndName() { + state.numbersAndNames.unshift( { name: "one", value: 1 } ); + } + }, +} ); + +store( 'directive-each', { + state: { + animalBreeds: [ + { name: "Dog", breeds: [ 'chihuahua', 'rottweiler' ] }, + { name: "Cat", breeds: [ 'sphynx', 'siamese' ] }, + ], + }, + actions: { + addAnimal() { + state.animalBreeds.unshift( { + name: "Rat", breeds: [ 'dumbo', 'rex' ] + } ); + }, + addBreeds() { + state + .animalBreeds + .forEach( ( { name, breeds } ) => { + if ( name === 'Dog') breeds.unshift( 'german shepherd' ); + if ( name === 'Cat') breeds.unshift( 'maine coon' ); + if ( name === 'Rat') breeds.unshift( 'satin' ); + } ); + } + } +} ); + +const html = ` +
+ + +

alpha

+

beta

+

gamma

+

delta

+
+`; + +store( 'directive-each', { + actions: { + navigate() { + return navigate( window.location, { + force: true, + html, + } ); + }, + } +} ); + diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index b37fff1bbb5c4c..252b200f9d4d01 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -12,6 +12,7 @@ - Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([#57805](https://github.com/WordPress/gutenberg/pull/57805)) - Add `wp-data-on-window` and `wp-data-on-document` directives. ([#57931](https://github.com/WordPress/gutenberg/pull/57931)) +- Add the `data-wp-each` directive to render lists of items using a template. ([57859](https://github.com/WordPress/gutenberg/pull/57859)) ### Breaking Changes diff --git a/packages/interactivity/docs/2-api-reference.md b/packages/interactivity/docs/2-api-reference.md index 922662c10a8e2b..bae15e9a7fcf2f 100644 --- a/packages/interactivity/docs/2-api-reference.md +++ b/packages/interactivity/docs/2-api-reference.md @@ -26,6 +26,7 @@ DOM elements are connected to data stored in the state and context through direc - [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-run`](#wp-run) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) + - [`wp-each`](#wp-each) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) - [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties) - [The store](#the-store) - [Elements of the store](#elements-of-the-store) @@ -620,6 +621,78 @@ But it can also be used on other elements: When the list is re-rendered, the Interactivity API will match elements by their keys to determine if an item was added/removed/reordered. Elements without keys might be recreated unnecessarily. + +#### `wp-each` + +The `wp-each` directive is intended to render a list of elements. The directive can be used in `