From 1bbe909c44e041f7bb221283a91b679f52eea17e Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 22 Feb 2024 12:40:18 +0100 Subject: [PATCH 1/6] Merge Interactivity docs --- .../assets/interactivity-state-directives.png | Bin packages/interactivity/README.md | 1097 ++++++++++++++++- .../interactivity/docs/1-getting-started.md | 87 -- .../interactivity/docs/2-api-reference.md | 973 --------------- packages/interactivity/docs/README.md | 33 - .../docs/assets/store-server-client.png | Bin 156828 -> 0 bytes 6 files changed, 1070 insertions(+), 1120 deletions(-) rename packages/interactivity/docs/assets/state-directives.png => docs/assets/interactivity-state-directives.png (100%) delete mode 100644 packages/interactivity/docs/1-getting-started.md delete mode 100644 packages/interactivity/docs/2-api-reference.md delete mode 100644 packages/interactivity/docs/README.md delete mode 100644 packages/interactivity/docs/assets/store-server-client.png diff --git a/packages/interactivity/docs/assets/state-directives.png b/docs/assets/interactivity-state-directives.png similarity index 100% rename from packages/interactivity/docs/assets/state-directives.png rename to docs/assets/interactivity-state-directives.png diff --git a/packages/interactivity/README.md b/packages/interactivity/README.md index 0ea49bdea0782f..5316aa56f74d81 100644 --- a/packages/interactivity/README.md +++ b/packages/interactivity/README.md @@ -1,62 +1,1105 @@ # Interactivity API -> **Warning** -> **This package is only available in Gutenberg** at the moment and not in WordPress Core as it is still very experimental, and very likely to change. +> **Note** +> This package enables the API shared at [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/). As part of an [Open Source project](https://developer.wordpress.org/block-editor/getting-started/faq/#the-gutenberg-project), participation is encouraged in testing this API providing feedback at the [discussions in GitHub](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api). + +The Interactivity API is available at WordPress Core from version 6.5 [Merge announcement](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/) + +These Core blocks are already powered by thi API: + +- Search +- Query +- Navigation +- File + +## Installation + +Install the module: + +```bash +npm install @wordpress/interactivity --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ + +## Quick Start Guide + +### Table of Contents + +- [Quick Start Guide](#quick-start-guide) + - [1. Scaffold an interactive block](#1-scaffold-an-interactive-block) + - [2. Generate the build](#2-generate-the-build) + - [3. Use it in your WordPress installation ](#3-use-it-in-your-wordpress-installation) +- [Requirements of the Interactivity API](#requirements-of-the-interactivity-aPI) + - [A local WordPress installation](#a-local-wordpress-installation) + - [Latest vesion of Gutenberg](#latest-vesion-of-gutenberg) + - [Node.js](#nodejs) + - [Code requirements](#code-requirements) + - [Add `interactivity` support to `block.json`](#add-interactivity-support-to-blockjson) + - [Add `wp-interactive` directive to a DOM element](#add-wp-interactive-directive-to-a-dom-element) + +#### 1. Scaffold an interactive block + +We can scaffold a WordPress plugin that registers an interactive block (using the Interactivity API) by using a [template](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) with the `@wordpress/create-block` command. + +``` +npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template +``` + +#### 2. Generate the build + +When the plugin folder is generated, we should launch the build process to get the final version of the interactive block that can be used from WordPress. + +``` +cd my-first-interactive-block && npm start +``` + +#### 3. Use it in your WordPress installation + +If you have a local WordPress installation already running, you can launch the commands above inside the `plugins` folder of that installation. If not, you can use [`wp-now`](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now) to launch a WordPress site with the plugin installed by executing from the generated folder (and from a different terminal window or tab) the following command + +``` +npx @wp-now/wp-now start +``` + +At this point you should be able to insert the "My First Interactive Block" block into any post, and see how it behaves in the frontend when published. + +### Requirements of the Interactivity API + +To start working with the Interactivity API you'll need to have a [proper WordPress development environment for blocks](https://developer.wordpress.org/block-editor/getting-started/devenv/) and some specific code in your block, which should include: + +#### A local 6.5 WordPress installation + +You can use [the tools to set your local WordPress environment](https://developer.wordpress.org/block-editor/getting-started/devenv/#wordpress-development-site) you feel more comfortable with. + +To get quickly started, [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now) is the easiest way to get a WordPress site up and running locally. + +Interactivity API is included in Core in WordPress 6.5, for versions below, you'll need to have Gutenberg 17.5 or higher version installed and activated in your WordPress installation. + +#### Node.js + +Block development requires [Node](https://nodejs.org/en), so you'll need to have Node installed and running on your machine. Any version modern should work, but please check the minimum version requirements if you run into any issues with any of the Node.js tools used in WordPress development. +#### Code requirements + +##### Add `interactivity` support to `block.json` + +To indicate that our block [supports](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) the Interactivity API features, we do so by adding `"interactivity": true` to the `supports` attribute of our block's `block.json` + +``` +"supports": { + "interactivity": true +}, +``` + +##### Add `wp-interactive` directive to a DOM element + +To "activate" the Interactivity API in a DOM element (and its children) we add the [`wp-interactive` directive](#wp-interactive) to it from our `render.php` or `save.js` + + +```html +
+ +
+``` + +## API Reference + +To add interactivity to blocks using the Interactivity API, developers can use: + +- **Directives** - added to the markup to add specific behavior to the DOM elements of the block. +- **Store** - that contains the logic and data (state, actions, or side effects, among others) needed for the behavior. + +DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see [diagram](https://excalidraw.com/#json=T4meh6lltJh6TCX51NTIu,DmIhxYSGFTL_ywZFbsmuSw)). + +![State & Directives](../../docs/assets/interactivity-state-directives.png) + +### Table of Contents + +- [The directives](#the-directives) + - [List of Directives](#list-of-directives) + - [`wp-interactive`](#wp-interactive) ![](https://img.shields.io/badge/DECLARATIVE-afd2e3.svg) + - [`wp-context`](#wp-context) ![](https://img.shields.io/badge/STATE-afd2e3.svg) + - [`wp-bind`](#wp-bind) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) + - [`wp-class`](#wp-class) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) + - [`wp-style`](#wp-style) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) + - [`wp-text`](#wp-text) ![](https://img.shields.io/badge/CONTENT-afd2e3.svg) + - [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) + - [`wp-on-window`](#wp-on-window) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) + - [`wp-on-document`](#wp-on-document) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) + - [`wp-watch`](#wp-watch) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) + - [`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) + - [State](#state) + - [Actions](#actions) + - [Side Effects](#side-effects) + - [Setting the store](#setting-the-store) + - [On the client side](#on-the-client-side) + - [On the server side](#on-the-server-side) + +### The directives + +Directives are custom attributes that are added to the markup of your block to add behavior to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks). + +Interactivity API directives use the `data-` prefix. + +_Example of directives used in the HTML markup_ + +```html +
+ + +

+ This element is now visible! +

+
+``` + +Directives can also be injected dynamically using the [HTML Tag Processor](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2). + +#### List of Directives + +With directives, we can directly manage interactions related to things such as side effects, state, event handlers, attributes or content. + +##### `wp-interactive` + +The `wp-interactive` directive "activates" the interactivity for the DOM element and its children through the Interactivity API (directives and store). The directive includes a namespace to reference a specific store, that can be set as a `string` or an `object`. + +```html + +
+

I'm interactive now, >and I can use directives!

+
+

I'm also interactive, and I can also use directives!

+
+
+```html + +
+

I'm interactive now, >and I can use directives!

+
+

I'm also interactive, and I can also use directives!

+
+
+``` > **Note** -> This package enables the API shared at [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/). As part of an [Open Source project](https://developer.wordpress.org/block-editor/getting-started/faq/#the-gutenberg-project) we encourage participation in helping shape this API and the [discussions in GitHub](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) is the best place to engage. +> The use of `data-wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `data-wp-interactive` has not been added for the sake of simplicity. Also, the `data-wp-interactive` directive will be injected automatically in the future. -This package can be tested, but it's still very experimental. -The Interactivity API is [being used in some core blocks](https://github.com/search?q=repo%3AWordPress%2Fgutenberg%20%40wordpress%2Finteractivity&type=code) but its use is still very limited. +##### `wp-context` +It provides a **local** state available to a specific HTML node and its children. -## Frequently Asked Questions +The `wp-context` directive accepts a stringified JSON as a value. -At this point, some of the questions you have about the Interactivity API may be: +_Example of `wp-context` directive_ -### What is this? +```php +//render.php +
+ +
+``` -This is the base of a new standard to create interactive blocks. Read [the proposal](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/) to learn more about this. +
+ See store used with the directive above -### Can I use it? +```js +store( "myPlugin", { + actions: { + logId: () => { + const { post } = getContext(); + console.log( post.id ); + }, + }, +} ); +``` -You can test it, but it's still very experimental. +
+
-### How do I get started? +Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one: -The best place to start with the Interactivity API is this [**Getting started guide**](https://github.com/WordPress/gutenberg/blob/trunk/packages/interactivity/docs/1-getting-started.md). There you'll will find a very quick start guide and the current requirements of the Interactivity API. +```html +
+ -### Where can I ask questions? +
+ -The [“Interactivity API” category](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) in Gutenberg repo discussions is the best place to ask questions about the Interactivity API. +
+ +
-### Where can I share my feedback about the API? +
+
+``` -The [“Interactivity API” category](https://github.com/WordPress/gutenberg/discussions/categories/interactivity-api) in Gutenberg repo discussions is also the best place to share your feedback about the Interactivity API. +##### `wp-bind` -## Installation +It allows setting HTML attributes on elements based on a boolean or string value. -Install the module: +> This directive follows the syntax `data-wp-bind--attribute`. -```bash -npm install @wordpress/interactivity --save +_Example of `wp-bind` directive_ + +```html +
  • + +
    + Title +
      + SUBMENU ITEMS +
    +
    +
  • ``` -_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ +
    + See store used with the directive above -## Docs & Examples +```js +store( "myPlugin", { + actions: { + toggleMenu: () => { + const context = getContext(); + context.isMenuOpen = !context.isMenuOpen; + }, + }, +} ); +``` + +
    +
    + +The `wp-bind` directive is executed: + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). + +When `wp-bind` directive references a callback to get its final value: + +- The `wp-bind` directive will be executed each time there's a change on any of the properties of the `state` or `context` used inside this callback. +- The returned value in the callback function is used to change the value of the associated attribute. + +The `wp-bind` will do different things over the DOM element is applied, depending on its value: + + - If the value is `true`, the attribute is added: `
    `. + - If the value is `false`, the attribute is removed: `
    `. + - If the value is a string, the attribute is added with its value assigned: `
    `. + +##### `wp-class` + +It adds or removes a class to an HTML element, depending on a boolean value. + +> This directive follows the syntax `data-wp-class--classname`. + +_Example of `wp-class` directive_ + +```html +
    +
  • + Option 1 +
  • +
  • + Option 2 +
  • +
    +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + actions: { + toggleSelection: () => { + const context = getContext(); + context.isSelected = !context.isSelected + } + } +} ); +``` + +
    +
    + +The `wp-class` directive is executed: + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). + +When `wp-class` directive references a callback to get its final boolean value, the callback receives the class name: `className`. + +The boolean value received by the directive is used to toggle (add when `true` or remove when `false`) the associated class name from the `class` attribute. + +##### `wp-style` + +It adds or removes inline style to an HTML element, depending on its value. + +> This directive follows the syntax `data-wp-style--css-property`. + +_Example of `wp-style` directive_ + +```html +
    + +

    Hello World!

    +
    +> +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + actions: { + toggleContextColor: () => { + const context = getContext(); + context.color = context.color === 'red' ? 'blue' : 'red'; + }, + }, +} ); +``` + +
    +
    + +The `wp-style` directive is executed: + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). + +When `wp-style` directive references a callback to get its final value, the callback receives the class style property: `css-property`. + +The value received by the directive is used to add or remove the style attribute with the associated CSS property: : + +- If the value is `false`, the style attribute is removed: `
    `. +- If the value is a string, the attribute is added with its value assigned: `
    `. + +##### `wp-text` + +It sets the inner text of an HTML element. + +```html +
    + + +
    +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + actions: { + toggleContextText: () => { + const context = getContext(); + context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1'; + }, + }, +} ); +``` + +
    +
    + +The `wp-text` directive is executed: + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). + +The returned value is used to change the inner content of the element: `
    value
    `. + +##### `wp-on` + +It runs code on dispatched DOM events like `click` or `keyup`. + +> The syntax of this directive is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`). + +_Example of `wp-on` directive_ + +```php + +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + actions: { + logTime: ( event ) => { + console.log( new Date() ) + }, + }, +} ); +``` + +
    +
    + +The `wp-on` directive is executed each time the associated event is triggered. + +The callback passed as the reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`), and the returned value by this callback is ignored. + +##### `wp-on-window` + +It allows to attach global window events like `resize`, `copy`, `focus` and then execute a defined callback when those happen. + +[List of supported window events.](https://developer.mozilla.org/en-US/docs/Web/API/Window#events) + +> The syntax of this directive is `data-wp-on-window--[window-event]` (like `data-wp-on-window--resize` +or `data-wp-on-window--languagechange`). + +_Example of `wp-on-window` directive_ + +```php +
    +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + callbacks: { + logWidth() { + console.log( 'Window width: ', window.innerWidth ); + }, + }, +} ); +``` + +
    +
    + +The callback passed as the reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`), and the returned value by this callback is ignored. When the element is removed from the DOM, the event listener is also removed. + +##### `wp-on-document` + +It allows to attach global document events like `scroll`, `mousemove`, `keydown` and then execute a defined callback when those happen. + +[List of supported document events.](https://developer.mozilla.org/en-US/docs/Web/API/Document#events) + +> The syntax of this directive is `data-wp-on-document--[document-event]` (like `data-wp-on-document--keydown` +or `data-wp-on-document--selectionchange`). + +_Example of `wp-on-document` directive_ + +```php +
    +``` + +
    + See store used with the directive above -**[Interactivity API Documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/interactivity/docs)** is the best place to learn about this proposal. Although it's still in progress, some key pages are already available: +```js +store( "myPlugin", { + callbacks: { + logKeydown(event) { + console.log( 'Key pressed: ', event.key ); + }, + }, +} ); +``` + +
    +
    + +The callback passed as the reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`), and the returned value by this callback is ignored. When the element is removed from the DOM, the event listener is also removed. + +##### `wp-watch` + +It runs a callback **when the node is created and runs it again when the state or context changes**. + +You can attach several side effects to the same DOM element by using the syntax `data-wp-watch--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-watch` directives of that DOM element._ + +_Example of `wp-watch` directive_ + +```html +
    +

    Counter:

    + + +
    +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + actions: { + increaseCounter: () => { + const context = getContext(); + context.counter++; + }, + decreaseCounter: () => { + const context = getContext(); + context.counter--; + }, + }, + callbacks: { + logCounter: () => { + const { counter } = getContext(); + console.log("Counter is " + counter + " at " + new Date() ); + }, + }, +} ); +``` + +
    +
    + +The `wp-watch` directive is executed: + +- When the element is created. +- Each time that any of the properties of the `state` or `context` used inside the callback changes. + +The `wp-watch` directive can return a function. If it does, the returned function is used as cleanup logic, i.e., it will run just before the callback runs again, and it will run again when the element is removed from the DOM. + +As a reference, some use cases for this directive may be: + +- Logging. +- Changing the title of the page. +- Setting the focus on an element with `.focus()`. +- Changing the state or context when certain conditions are met. + +##### `wp-init` + +It runs a callback **only when the node is created**. + +You can attach several `wp-init` to the same DOM element by using the syntax `data-wp-init--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-init` directives of that DOM element._ + +_Example of `data-wp-init` directive_ + +```html +
    +

    Hi! +

    +``` + +_Example of several `wp-init` directives on the same DOM element_ + +```html +
    + +
    +``` + +
    + See store used with the directive above + +```js +store( "myPlugin", { + callbacks: { + logTimeInit: () => console.log( `Init at ` + new Date() ), + focusFirstElement: () => { + const { ref } = getElement(); + ref.querySelector( 'input:first-child' ).focus(), + }, + }, +} ); +``` + +
    +
    + +The `wp-init` can return a function. If it does, the returned function will run when the element is removed from the DOM. + +##### `wp-run` + +It runs the passed callback **during node's render execution**. + +You can use and compose hooks like `useState`, `useWatch` or `useEffect` inside inside the passed callback and create your own logic, providing more flexibility than previous directives. + +You can attach several `wp-run` to the same DOM element by using the syntax `data-wp-run--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-run` directives of that DOM element._ + +_Example of `data-wp-run` directive_ + +```html +
    +

    Hi!

    +
    +``` + +
    + See store used with the directive above + +```js +import { store, useState, useEffect } from '@wordpress/interactivity'; + +// Unlike `data-wp-init` and `data-wp-watch`, you can use any hooks inside +// `data-wp-run` callbacks. +const useInView = ( ref ) => { + const [ inView, setInView ] = useState( false ); + useEffect( () => { + const observer = new IntersectionObserver( ( [ entry ] ) => { + setInView( entry.isIntersecting ); + } ); + if ( ref ) observer.observe( ref ); + return () => ref && observer.unobserve( ref ); + }, []); + return inView; +}; + +store( 'myPlugin', { + callbacks: { + logInView: () => { + const { ref } = getElement(); + const isInView = useInView( ref ); + useEffect( () => { + if ( isInView ) { + console.log( 'Inside' ); + } else { + console.log( 'Outside' ); + } + }); + } + }, +} ); +``` + +
    +
    + +##### `wp-key` + +The `wp-key` directive assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements. This becomes important if your array elements can move (e.g., due to sorting), get inserted, or get deleted. A well-chosen key value helps the Interactivity API infer what exactly has changed in the array, allowing it to make the correct updates to the DOM. + +The key should be a string that uniquely identifies the element among its siblings. Typically, it is used on repeated elements like list items. For example: + +```html +
      +
    • Item 1
    • +
    • Item 2
    • +
    +``` + +But it can also be used on other elements: + +```html + +``` + +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 `