@wordpress/interactivity
Advanced tools
Comparing version 3.0.0 to 3.0.1
import { createElement } from "react"; | ||
// @ts-nocheck | ||
/** | ||
@@ -14,29 +12,2 @@ * External dependencies | ||
import { stores } from './store'; | ||
/** @typedef {import('preact').VNode} VNode */ | ||
/** @typedef {typeof context} Context */ | ||
/** @typedef {ReturnType<typeof getEvaluate>} Evaluate */ | ||
/** | ||
* @typedef {Object} DirectiveCallbackParams Callback parameters. | ||
* @property {Object} directives Object map with the defined directives of the element being evaluated. | ||
* @property {Object} props Props present in the current element. | ||
* @property {VNode} element Virtual node representing the original element. | ||
* @property {Context} context The inherited context. | ||
* @property {Evaluate} evaluate Function that resolves a given path to a value either in the store or the context. | ||
*/ | ||
/** | ||
* @callback DirectiveCallback Callback that runs the directive logic. | ||
* @param {DirectiveCallbackParams} params Callback parameters. | ||
*/ | ||
/** | ||
* @typedef DirectiveOptions Options object. | ||
* @property {number} [priority=10] Value that specifies the priority to | ||
* evaluate directives of this type. Lower | ||
* numbers correspond with earlier execution. | ||
* Default is `10`. | ||
*/ | ||
// Main context. | ||
@@ -67,3 +38,21 @@ const context = createContext({}); | ||
const namespaceStack = []; | ||
/** | ||
* Retrieves the context inherited by the element evaluating a function from the | ||
* store. The returned value depends on the element and the namespace where the | ||
* function calling `getContext()` exists. | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The context content. | ||
*/ | ||
export const getContext = namespace => getScope()?.context[namespace || namespaceStack.slice(-1)[0]]; | ||
/** | ||
* Retrieves a representation of the element where a function from the store | ||
* is being evalutated. Such representation is read-only, and contains a | ||
* reference to the DOM element, its props and a local reactive state. | ||
* | ||
* @return Element representation. | ||
*/ | ||
export const getElement = () => { | ||
@@ -109,6 +98,5 @@ if (!getScope()) { | ||
* 'alert', // Name without the `data-wp-` prefix. | ||
* ( { directives: { alert }, element, evaluate }) => { | ||
* element.props.onclick = () => { | ||
* alert( evaluate( alert.default ) ); | ||
* } | ||
* ( { directives: { alert }, element, evaluate } ) => { | ||
* const defaultEntry = alert.find( entry => entry.suffix === 'default' ); | ||
* element.props.onclick = () => { alert( evaluate( defaultEntry ) ); } | ||
* } | ||
@@ -120,3 +108,3 @@ * ) | ||
* message whenever an element using it is clicked. The message text is obtained | ||
* from the store using `evaluate`. | ||
* from the store under the inherited namespace, using `evaluate`. | ||
* | ||
@@ -127,14 +115,18 @@ * When the HTML is processed by the Interactivity API, any element containing | ||
* ```html | ||
* <button data-wp-alert="state.messages.alert">Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "messages" }'> | ||
* <button data-wp-alert="state.alert">Click me!</button> | ||
* </div> | ||
* ``` | ||
* Note that, in the previous example, you access `alert.default` in order to | ||
* retrieve the `state.messages.alert` value passed to the directive. You can | ||
* also define custom names by appending `--` to the directive attribute, | ||
* followed by a suffix, like in the following HTML snippet: | ||
* Note that, in the previous example, the directive callback gets the path | ||
* value (`state.alert`) from the directive entry with suffix `default`. A | ||
* custom suffix can also be specified by appending `--` to the directive | ||
* attribute, followed by the suffix, like in the following HTML snippet: | ||
* | ||
* ```html | ||
* <button | ||
* data-wp-color--text="state.theme.text" | ||
* data-wp-color--background="state.theme.background" | ||
* >Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "myblock" }'> | ||
* <button | ||
* data-wp-color--text="state.text" | ||
* data-wp-color--background="state.background" | ||
* >Click me!</button> | ||
* </div> | ||
* ``` | ||
@@ -149,15 +141,17 @@ * | ||
* 'color', // Name without prefix and suffix. | ||
* ( { directives: { color }, ref, evaluate }) => { | ||
* if ( color.text ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.background ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* ( { directives: { color }, ref, evaluate } ) => | ||
* colors.forEach( ( color ) => { | ||
* if ( color.suffix = 'text' ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.suffix = 'background' ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* } ); | ||
* } | ||
@@ -167,5 +161,7 @@ * ) | ||
* | ||
* @param {string} name Directive name, without the `data-wp-` prefix. | ||
* @param {DirectiveCallback} callback Function that runs the directive logic. | ||
* @param {DirectiveOptions=} options Options object. | ||
* @param name Directive name, without the `data-wp-` prefix. | ||
* @param callback Function that runs the directive logic. | ||
* @param options Options object. | ||
* @param options.priority Option to control the directive execution order. The | ||
* lesser, the highest priority. Default is `10`. | ||
*/ | ||
@@ -192,3 +188,3 @@ export const directive = (name, callback, { | ||
scope | ||
} = {}) => (entry, ...args) => { | ||
}) => (entry, ...args) => { | ||
let { | ||
@@ -198,2 +194,5 @@ value: path, | ||
} = entry; | ||
if (typeof path !== 'string') { | ||
throw new Error('The `value` prop should be a string path'); | ||
} | ||
// If path starts with !, remove it and save a flag. | ||
@@ -218,3 +217,3 @@ const hasNegationOperator = path[0] === '!' && !!(path = path.slice(1)); | ||
}, {}); | ||
return Object.entries(byPriority).sort(([p1], [p2]) => p1 - p2).map(([, arr]) => arr); | ||
return Object.entries(byPriority).sort(([p1], [p2]) => parseInt(p1) - parseInt(p2)).map(([, arr]) => arr); | ||
}; | ||
@@ -228,3 +227,3 @@ | ||
originalProps, | ||
previousScope = {} | ||
previousScope | ||
}) => { | ||
@@ -240,4 +239,4 @@ // Initialize the scope of this element. These scopes are different per each | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
scope.ref = previousScope.ref || useRef(null); | ||
scope.state = previousScope.state || useRef(deepSignal({})).current; | ||
scope.ref = previousScope?.ref || useRef(null); | ||
scope.state = previousScope?.state || useRef(deepSignal({})).current; | ||
/* eslint-enable react-hooks/rules-of-hooks */ | ||
@@ -244,0 +243,0 @@ |
@@ -64,4 +64,14 @@ /** | ||
// Prefetch a page. We store the promise to avoid triggering a second fetch for | ||
// a page if a fetching has already started. | ||
/** | ||
* Prefetchs the page with the passed URL. | ||
* | ||
* The function normalizes the URL and stores internally the fetch promise, to | ||
* avoid triggering a second fetch for an ongoing request. | ||
* | ||
* @param {string} url The page URL. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] Force fetching the URL again. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
*/ | ||
export const prefetch = (url, options = {}) => { | ||
@@ -90,3 +100,22 @@ url = cleanUrl(url); | ||
// Navigate to a new page. | ||
/** | ||
* Navigates to the specified page. | ||
* | ||
* This function normalizes the passed href, fetchs the page HTML if needed, and | ||
* updates any interactive regions whose contents have changed. It also creates | ||
* a new entry in the browser session history. | ||
* | ||
* @param {string} href The page href. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] If true, it forces re-fetching the URL. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
* @param {boolean} [options.replace] If true, it replaces the current entry in | ||
* the browser session history. | ||
* @param {number} [options.timeout] Time until the navigation is aborted, in | ||
* milliseconds. Default is 10000. | ||
* | ||
* @return {Promise} Promise that resolves once the navigation is completed or | ||
* aborted. | ||
*/ | ||
export const navigate = async (href, options = {}) => { | ||
@@ -93,0 +122,0 @@ const url = cleanUrl(href); |
@@ -145,35 +145,28 @@ /** | ||
}; | ||
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
/** | ||
* @typedef StoreProps Properties object passed to `store`. | ||
* @property {Object} state State to be added to the global store. All the | ||
* properties included here become reactive. | ||
*/ | ||
/** | ||
* @typedef StoreOptions Options object. | ||
*/ | ||
/** | ||
* Extends the Interactivity API global store with the passed properties. | ||
* Extends the Interactivity API global store adding the passed properties to | ||
* the given namespace. It also returns stable references to the namespace | ||
* content. | ||
* | ||
* These props typically consist of `state`, which is reactive, and other | ||
* properties like `selectors`, `actions`, `effects`, etc. which can store | ||
* callbacks and derived state. These props can then be referenced by any | ||
* directive to make the HTML interactive. | ||
* These props typically consist of `state`, which is the reactive part of the | ||
* store ― which means that any directive referencing a state property will be | ||
* re-rendered anytime it changes ― and function properties like `actions` and | ||
* `callbacks`, mostly used for event handlers. These props can then be | ||
* referenced by any directive to make the HTML interactive. | ||
* | ||
* @example | ||
* ```js | ||
* store({ | ||
* const { state } = store( 'counter', { | ||
* state: { | ||
* counter: { value: 0 }, | ||
* value: 0, | ||
* get double() { return state.value * 2; }, | ||
* }, | ||
* actions: { | ||
* counter: { | ||
* increment: ({ state }) => { | ||
* state.counter.value += 1; | ||
* }, | ||
* increment() { | ||
* state.value += 1; | ||
* }, | ||
* }, | ||
* }); | ||
* } ); | ||
* ``` | ||
@@ -185,6 +178,6 @@ * | ||
* ```html | ||
* <div data-wp-interactive> | ||
* <div data-wp-interactive='{ "namespace": "counter" }'> | ||
* <button | ||
* data-wp-text="state.counter.value" | ||
* data-wp-on--click="actions.counter.increment" | ||
* data-wp-text="state.double" | ||
* data-wp-on--click="actions.increment" | ||
* > | ||
@@ -195,8 +188,9 @@ * 0 | ||
* ``` | ||
* @param namespace The store namespace to interact with. | ||
* @param storePart Properties to add to the store namespace. | ||
* @param options Options for the given namespace. | ||
* | ||
* @param {StoreProps} properties Properties to be added to the global store. | ||
* @param {StoreOptions} [options] Options passed to the `store` call. | ||
* @return A reference to the namespace content. | ||
*/ | ||
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
export function store(namespace, { | ||
@@ -203,0 +197,0 @@ state = {}, |
@@ -0,9 +1,77 @@ | ||
import type { VNode, Context, RefObject } from 'preact'; | ||
interface DirectiveEntry { | ||
value: string | Object; | ||
namespace: string; | ||
suffix: string; | ||
} | ||
type DirectiveEntries = Record<string, DirectiveEntry[]>; | ||
interface DirectiveArgs { | ||
/** | ||
* Object map with the defined directives of the element being evaluated. | ||
*/ | ||
directives: DirectiveEntries; | ||
/** | ||
* Props present in the current element. | ||
*/ | ||
props: Object; | ||
/** | ||
* Virtual node representing the element. | ||
*/ | ||
element: VNode; | ||
/** | ||
* The inherited context. | ||
*/ | ||
context: Context<any>; | ||
/** | ||
* Function that resolves a given path to a value either in the store or the | ||
* context. | ||
*/ | ||
evaluate: Evaluate; | ||
} | ||
interface DirectiveCallback { | ||
(args: DirectiveArgs): VNode | void; | ||
} | ||
interface DirectiveOptions { | ||
/** | ||
* Value that specifies the priority to evaluate directives of this type. | ||
* Lower numbers correspond with earlier execution. | ||
* | ||
* @default 10 | ||
*/ | ||
priority?: number; | ||
} | ||
interface Scope { | ||
evaluate: Evaluate; | ||
context: Context<any>; | ||
ref: RefObject<HTMLElement>; | ||
state: any; | ||
props: any; | ||
} | ||
interface Evaluate { | ||
(entry: DirectiveEntry, ...args: any[]): any; | ||
} | ||
/** | ||
* Retrieves the context inherited by the element evaluating a function from the | ||
* store. The returned value depends on the element and the namespace where the | ||
* function calling `getContext()` exists. | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The context content. | ||
*/ | ||
export declare const getContext: <T extends object>(namespace?: string) => T; | ||
/** | ||
* Retrieves a representation of the element where a function from the store | ||
* is being evalutated. Such representation is read-only, and contains a | ||
* reference to the DOM element, its props and a local reactive state. | ||
* | ||
* @return Element representation. | ||
*/ | ||
export declare const getElement: () => Readonly<{ | ||
ref: any; | ||
ref: HTMLElement; | ||
state: any; | ||
props: any; | ||
}>; | ||
export declare const getScope: () => any; | ||
export declare const setScope: (scope: any) => void; | ||
export declare const getScope: () => Scope; | ||
export declare const setScope: (scope: Scope) => void; | ||
export declare const resetScope: () => void; | ||
@@ -19,6 +87,5 @@ export declare const setNamespace: (namespace: string) => void; | ||
* 'alert', // Name without the `data-wp-` prefix. | ||
* ( { directives: { alert }, element, evaluate }) => { | ||
* element.props.onclick = () => { | ||
* alert( evaluate( alert.default ) ); | ||
* } | ||
* ( { directives: { alert }, element, evaluate } ) => { | ||
* const defaultEntry = alert.find( entry => entry.suffix === 'default' ); | ||
* element.props.onclick = () => { alert( evaluate( defaultEntry ) ); } | ||
* } | ||
@@ -30,3 +97,3 @@ * ) | ||
* message whenever an element using it is clicked. The message text is obtained | ||
* from the store using `evaluate`. | ||
* from the store under the inherited namespace, using `evaluate`. | ||
* | ||
@@ -37,14 +104,18 @@ * When the HTML is processed by the Interactivity API, any element containing | ||
* ```html | ||
* <button data-wp-alert="state.messages.alert">Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "messages" }'> | ||
* <button data-wp-alert="state.alert">Click me!</button> | ||
* </div> | ||
* ``` | ||
* Note that, in the previous example, you access `alert.default` in order to | ||
* retrieve the `state.messages.alert` value passed to the directive. You can | ||
* also define custom names by appending `--` to the directive attribute, | ||
* followed by a suffix, like in the following HTML snippet: | ||
* Note that, in the previous example, the directive callback gets the path | ||
* value (`state.alert`) from the directive entry with suffix `default`. A | ||
* custom suffix can also be specified by appending `--` to the directive | ||
* attribute, followed by the suffix, like in the following HTML snippet: | ||
* | ||
* ```html | ||
* <button | ||
* data-wp-color--text="state.theme.text" | ||
* data-wp-color--background="state.theme.background" | ||
* >Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "myblock" }'> | ||
* <button | ||
* data-wp-color--text="state.text" | ||
* data-wp-color--background="state.background" | ||
* >Click me!</button> | ||
* </div> | ||
* ``` | ||
@@ -59,15 +130,17 @@ * | ||
* 'color', // Name without prefix and suffix. | ||
* ( { directives: { color }, ref, evaluate }) => { | ||
* if ( color.text ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.background ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* ( { directives: { color }, ref, evaluate } ) => | ||
* colors.forEach( ( color ) => { | ||
* if ( color.suffix = 'text' ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.suffix = 'background' ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* } ); | ||
* } | ||
@@ -77,9 +150,10 @@ * ) | ||
* | ||
* @param {string} name Directive name, without the `data-wp-` prefix. | ||
* @param {DirectiveCallback} callback Function that runs the directive logic. | ||
* @param {DirectiveOptions=} options Options object. | ||
* @param name Directive name, without the `data-wp-` prefix. | ||
* @param callback Function that runs the directive logic. | ||
* @param options Options object. | ||
* @param options.priority Option to control the directive execution order. The | ||
* lesser, the highest priority. Default is `10`. | ||
*/ | ||
export declare const directive: (name: any, callback: any, { priority }?: { | ||
priority?: number; | ||
}) => void; | ||
export declare const directive: (name: string, callback: DirectiveCallback, { priority }?: DirectiveOptions) => void; | ||
export {}; | ||
//# sourceMappingURL=hooks.d.ts.map |
@@ -1,4 +0,12 @@ | ||
export function prefetch(url: any, options?: {}): void; | ||
export function navigate(href: any, options?: {}): Promise<void>; | ||
export function prefetch(url: string, options?: { | ||
force?: boolean; | ||
html?: string; | ||
}): void; | ||
export function navigate(href: string, options?: { | ||
force?: boolean; | ||
html?: string; | ||
replace?: boolean; | ||
timeout?: number; | ||
}): Promise<any>; | ||
export function init(): Promise<void>; | ||
//# sourceMappingURL=router.d.ts.map |
export declare const stores: Map<any, any>; | ||
interface StoreOptions { | ||
/** | ||
* Property to block/unblock private store namespaces. | ||
* | ||
* If the passed value is `true`, it blocks the given namespace, making it | ||
* accessible only trough the returned variables of the `store()` call. In | ||
* the case a lock string is passed, it also blocks the namespace, but can | ||
* be unblocked for other `store()` calls using the same lock string. | ||
* | ||
* @example | ||
* ``` | ||
* // The store can only be accessed where the `state` const can. | ||
* const { state } = store( 'myblock/private', { ... }, { lock: true } ); | ||
* ``` | ||
* | ||
* @example | ||
* ``` | ||
* // Other modules knowing `SECRET_LOCK_STRING` can access the namespace. | ||
* const { state } = store( | ||
* 'myblock/private', | ||
* { ... }, | ||
* { lock: 'SECRET_LOCK_STRING' } | ||
* ); | ||
* ``` | ||
*/ | ||
lock?: boolean | string; | ||
} | ||
/** | ||
* @typedef StoreProps Properties object passed to `store`. | ||
* @property {Object} state State to be added to the global store. All the | ||
* properties included here become reactive. | ||
*/ | ||
/** | ||
* @typedef StoreOptions Options object. | ||
*/ | ||
/** | ||
* Extends the Interactivity API global store with the passed properties. | ||
* Extends the Interactivity API global store adding the passed properties to | ||
* the given namespace. It also returns stable references to the namespace | ||
* content. | ||
* | ||
* These props typically consist of `state`, which is reactive, and other | ||
* properties like `selectors`, `actions`, `effects`, etc. which can store | ||
* callbacks and derived state. These props can then be referenced by any | ||
* directive to make the HTML interactive. | ||
* These props typically consist of `state`, which is the reactive part of the | ||
* store ― which means that any directive referencing a state property will be | ||
* re-rendered anytime it changes ― and function properties like `actions` and | ||
* `callbacks`, mostly used for event handlers. These props can then be | ||
* referenced by any directive to make the HTML interactive. | ||
* | ||
* @example | ||
* ```js | ||
* store({ | ||
* const { state } = store( 'counter', { | ||
* state: { | ||
* counter: { value: 0 }, | ||
* value: 0, | ||
* get double() { return state.value * 2; }, | ||
* }, | ||
* actions: { | ||
* counter: { | ||
* increment: ({ state }) => { | ||
* state.counter.value += 1; | ||
* }, | ||
* increment() { | ||
* state.value += 1; | ||
* }, | ||
* }, | ||
* }); | ||
* } ); | ||
* ``` | ||
@@ -38,6 +59,6 @@ * | ||
* ```html | ||
* <div data-wp-interactive> | ||
* <div data-wp-interactive='{ "namespace": "counter" }'> | ||
* <button | ||
* data-wp-text="state.counter.value" | ||
* data-wp-on--click="actions.counter.increment" | ||
* data-wp-text="state.double" | ||
* data-wp-on--click="actions.increment" | ||
* > | ||
@@ -48,9 +69,8 @@ * 0 | ||
* ``` | ||
* @param namespace The store namespace to interact with. | ||
* @param storePart Properties to add to the store namespace. | ||
* @param options Options for the given namespace. | ||
* | ||
* @param {StoreProps} properties Properties to be added to the global store. | ||
* @param {StoreOptions} [options] Options passed to the `store` call. | ||
* @return A reference to the namespace content. | ||
*/ | ||
interface StoreOptions { | ||
lock?: boolean | string; | ||
} | ||
export declare function store<S extends object = {}>(namespace: string, storePart?: S, options?: StoreOptions): S; | ||
@@ -57,0 +77,0 @@ export declare function store<T extends object>(namespace: string, storePart?: T, options?: StoreOptions): T; |
@@ -12,4 +12,2 @@ "use strict"; | ||
var _store = require("./store"); | ||
// @ts-nocheck | ||
/** | ||
@@ -23,28 +21,2 @@ * External dependencies | ||
/** @typedef {import('preact').VNode} VNode */ | ||
/** @typedef {typeof context} Context */ | ||
/** @typedef {ReturnType<typeof getEvaluate>} Evaluate */ | ||
/** | ||
* @typedef {Object} DirectiveCallbackParams Callback parameters. | ||
* @property {Object} directives Object map with the defined directives of the element being evaluated. | ||
* @property {Object} props Props present in the current element. | ||
* @property {VNode} element Virtual node representing the original element. | ||
* @property {Context} context The inherited context. | ||
* @property {Evaluate} evaluate Function that resolves a given path to a value either in the store or the context. | ||
*/ | ||
/** | ||
* @callback DirectiveCallback Callback that runs the directive logic. | ||
* @param {DirectiveCallbackParams} params Callback parameters. | ||
*/ | ||
/** | ||
* @typedef DirectiveOptions Options object. | ||
* @property {number} [priority=10] Value that specifies the priority to | ||
* evaluate directives of this type. Lower | ||
* numbers correspond with earlier execution. | ||
* Default is `10`. | ||
*/ | ||
// Main context. | ||
@@ -75,3 +47,21 @@ const context = (0, _preact.createContext)({}); | ||
const namespaceStack = []; | ||
/** | ||
* Retrieves the context inherited by the element evaluating a function from the | ||
* store. The returned value depends on the element and the namespace where the | ||
* function calling `getContext()` exists. | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The context content. | ||
*/ | ||
const getContext = namespace => getScope()?.context[namespace || namespaceStack.slice(-1)[0]]; | ||
/** | ||
* Retrieves a representation of the element where a function from the store | ||
* is being evalutated. Such representation is read-only, and contains a | ||
* reference to the DOM element, its props and a local reactive state. | ||
* | ||
* @return Element representation. | ||
*/ | ||
exports.getContext = getContext; | ||
@@ -124,6 +114,5 @@ const getElement = () => { | ||
* 'alert', // Name without the `data-wp-` prefix. | ||
* ( { directives: { alert }, element, evaluate }) => { | ||
* element.props.onclick = () => { | ||
* alert( evaluate( alert.default ) ); | ||
* } | ||
* ( { directives: { alert }, element, evaluate } ) => { | ||
* const defaultEntry = alert.find( entry => entry.suffix === 'default' ); | ||
* element.props.onclick = () => { alert( evaluate( defaultEntry ) ); } | ||
* } | ||
@@ -135,3 +124,3 @@ * ) | ||
* message whenever an element using it is clicked. The message text is obtained | ||
* from the store using `evaluate`. | ||
* from the store under the inherited namespace, using `evaluate`. | ||
* | ||
@@ -142,14 +131,18 @@ * When the HTML is processed by the Interactivity API, any element containing | ||
* ```html | ||
* <button data-wp-alert="state.messages.alert">Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "messages" }'> | ||
* <button data-wp-alert="state.alert">Click me!</button> | ||
* </div> | ||
* ``` | ||
* Note that, in the previous example, you access `alert.default` in order to | ||
* retrieve the `state.messages.alert` value passed to the directive. You can | ||
* also define custom names by appending `--` to the directive attribute, | ||
* followed by a suffix, like in the following HTML snippet: | ||
* Note that, in the previous example, the directive callback gets the path | ||
* value (`state.alert`) from the directive entry with suffix `default`. A | ||
* custom suffix can also be specified by appending `--` to the directive | ||
* attribute, followed by the suffix, like in the following HTML snippet: | ||
* | ||
* ```html | ||
* <button | ||
* data-wp-color--text="state.theme.text" | ||
* data-wp-color--background="state.theme.background" | ||
* >Click me!</button> | ||
* <div data-wp-interactive='{ "namespace": "myblock" }'> | ||
* <button | ||
* data-wp-color--text="state.text" | ||
* data-wp-color--background="state.background" | ||
* >Click me!</button> | ||
* </div> | ||
* ``` | ||
@@ -164,15 +157,17 @@ * | ||
* 'color', // Name without prefix and suffix. | ||
* ( { directives: { color }, ref, evaluate }) => { | ||
* if ( color.text ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.background ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* ( { directives: { color }, ref, evaluate } ) => | ||
* colors.forEach( ( color ) => { | ||
* if ( color.suffix = 'text' ) { | ||
* ref.style.setProperty( | ||
* 'color', | ||
* evaluate( color.text ) | ||
* ); | ||
* } | ||
* if ( color.suffix = 'background' ) { | ||
* ref.style.setProperty( | ||
* 'background-color', | ||
* evaluate( color.background ) | ||
* ); | ||
* } | ||
* } ); | ||
* } | ||
@@ -182,5 +177,7 @@ * ) | ||
* | ||
* @param {string} name Directive name, without the `data-wp-` prefix. | ||
* @param {DirectiveCallback} callback Function that runs the directive logic. | ||
* @param {DirectiveOptions=} options Options object. | ||
* @param name Directive name, without the `data-wp-` prefix. | ||
* @param callback Function that runs the directive logic. | ||
* @param options Options object. | ||
* @param options.priority Option to control the directive execution order. The | ||
* lesser, the highest priority. Default is `10`. | ||
*/ | ||
@@ -208,3 +205,3 @@ const directive = (name, callback, { | ||
scope | ||
} = {}) => (entry, ...args) => { | ||
}) => (entry, ...args) => { | ||
let { | ||
@@ -214,2 +211,5 @@ value: path, | ||
} = entry; | ||
if (typeof path !== 'string') { | ||
throw new Error('The `value` prop should be a string path'); | ||
} | ||
// If path starts with !, remove it and save a flag. | ||
@@ -234,3 +234,3 @@ const hasNegationOperator = path[0] === '!' && !!(path = path.slice(1)); | ||
}, {}); | ||
return Object.entries(byPriority).sort(([p1], [p2]) => p1 - p2).map(([, arr]) => arr); | ||
return Object.entries(byPriority).sort(([p1], [p2]) => parseInt(p1) - parseInt(p2)).map(([, arr]) => arr); | ||
}; | ||
@@ -244,3 +244,3 @@ | ||
originalProps, | ||
previousScope = {} | ||
previousScope | ||
}) => { | ||
@@ -256,4 +256,4 @@ // Initialize the scope of this element. These scopes are different per each | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
scope.ref = previousScope.ref || (0, _hooks.useRef)(null); | ||
scope.state = previousScope.state || (0, _hooks.useRef)((0, _deepsignal.deepSignal)({})).current; | ||
scope.ref = previousScope?.ref || (0, _hooks.useRef)(null); | ||
scope.state = previousScope?.state || (0, _hooks.useRef)((0, _deepsignal.deepSignal)({})).current; | ||
/* eslint-enable react-hooks/rules-of-hooks */ | ||
@@ -260,0 +260,0 @@ |
@@ -71,4 +71,14 @@ "use strict"; | ||
// Prefetch a page. We store the promise to avoid triggering a second fetch for | ||
// a page if a fetching has already started. | ||
/** | ||
* Prefetchs the page with the passed URL. | ||
* | ||
* The function normalizes the URL and stores internally the fetch promise, to | ||
* avoid triggering a second fetch for an ongoing request. | ||
* | ||
* @param {string} url The page URL. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] Force fetching the URL again. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
*/ | ||
const prefetch = (url, options = {}) => { | ||
@@ -98,3 +108,22 @@ url = cleanUrl(url); | ||
// Navigate to a new page. | ||
/** | ||
* Navigates to the specified page. | ||
* | ||
* This function normalizes the passed href, fetchs the page HTML if needed, and | ||
* updates any interactive regions whose contents have changed. It also creates | ||
* a new entry in the browser session history. | ||
* | ||
* @param {string} href The page href. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] If true, it forces re-fetching the URL. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
* @param {boolean} [options.replace] If true, it replaces the current entry in | ||
* the browser session history. | ||
* @param {number} [options.timeout] Time until the navigation is aborted, in | ||
* milliseconds. Default is 10000. | ||
* | ||
* @return {Promise} Promise that resolves once the navigation is completed or | ||
* aborted. | ||
*/ | ||
const navigate = async (href, options = {}) => { | ||
@@ -101,0 +130,0 @@ const url = cleanUrl(href); |
@@ -154,35 +154,28 @@ "use strict"; | ||
}; | ||
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
/** | ||
* @typedef StoreProps Properties object passed to `store`. | ||
* @property {Object} state State to be added to the global store. All the | ||
* properties included here become reactive. | ||
*/ | ||
/** | ||
* @typedef StoreOptions Options object. | ||
*/ | ||
/** | ||
* Extends the Interactivity API global store with the passed properties. | ||
* Extends the Interactivity API global store adding the passed properties to | ||
* the given namespace. It also returns stable references to the namespace | ||
* content. | ||
* | ||
* These props typically consist of `state`, which is reactive, and other | ||
* properties like `selectors`, `actions`, `effects`, etc. which can store | ||
* callbacks and derived state. These props can then be referenced by any | ||
* directive to make the HTML interactive. | ||
* These props typically consist of `state`, which is the reactive part of the | ||
* store ― which means that any directive referencing a state property will be | ||
* re-rendered anytime it changes ― and function properties like `actions` and | ||
* `callbacks`, mostly used for event handlers. These props can then be | ||
* referenced by any directive to make the HTML interactive. | ||
* | ||
* @example | ||
* ```js | ||
* store({ | ||
* const { state } = store( 'counter', { | ||
* state: { | ||
* counter: { value: 0 }, | ||
* value: 0, | ||
* get double() { return state.value * 2; }, | ||
* }, | ||
* actions: { | ||
* counter: { | ||
* increment: ({ state }) => { | ||
* state.counter.value += 1; | ||
* }, | ||
* increment() { | ||
* state.value += 1; | ||
* }, | ||
* }, | ||
* }); | ||
* } ); | ||
* ``` | ||
@@ -194,6 +187,6 @@ * | ||
* ```html | ||
* <div data-wp-interactive> | ||
* <div data-wp-interactive='{ "namespace": "counter" }'> | ||
* <button | ||
* data-wp-text="state.counter.value" | ||
* data-wp-on--click="actions.counter.increment" | ||
* data-wp-text="state.double" | ||
* data-wp-on--click="actions.increment" | ||
* > | ||
@@ -204,8 +197,9 @@ * 0 | ||
* ``` | ||
* @param namespace The store namespace to interact with. | ||
* @param storePart Properties to add to the store namespace. | ||
* @param options Options for the given namespace. | ||
* | ||
* @param {StoreProps} properties Properties to be added to the global store. | ||
* @param {StoreOptions} [options] Options passed to the `store` call. | ||
* @return A reference to the namespace content. | ||
*/ | ||
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
function store(namespace, { | ||
@@ -212,0 +206,0 @@ state = {}, |
@@ -86,5 +86,5 @@ # Getting started with the Interactivity API | ||
```html | ||
<div data-wp-interactive> | ||
<div data-wp-interactive='{ "namespace": "myPlugin" }'> | ||
<!-- Interactivity API zone --> | ||
</div> | ||
``` |
@@ -5,6 +5,6 @@ # API Reference | ||
- **Directives** - added to the markup to add specific behavior to the DOM elements of block. | ||
- **Store** - that contains the logic and data (state, actions, or effects among others) needed for the behaviour. | ||
- **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 & 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=rEg5d71O_jy3NrgYJUIVd,yjOUmMvxzNf6alqFjElvIw)). | ||
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)). | ||
@@ -24,3 +24,3 @@ ![State & Directives](assets/state-directives.png) | ||
- [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) | ||
- [`wp-effect`](#wp-effect) ![](https://img.shields.io/badge/SIDE_EFFECTS-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) | ||
@@ -33,5 +33,3 @@ - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) | ||
- [Actions](#actions) | ||
- [Effects](#effects) | ||
- [Selectors](#selectors) | ||
- [Arguments passed to callbacks](#arguments-passed-to-callbacks) | ||
- [Side Effects](#side-effects) | ||
- [Setting the store](#setting-the-store) | ||
@@ -41,7 +39,5 @@ - [On the client side](#on-the-client-side) | ||
## The directives | ||
Directives are custom attributes that are added to the markup of your block to add behaviour to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks). | ||
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). | ||
@@ -54,8 +50,9 @@ Interactivity API directives use the `data-` prefix. | ||
<div | ||
data-wp-context='{ "myNamespace" : { "isOpen": false } }' | ||
data-wp-effect="effects.myNamespace.logIsOpen" | ||
data-wp-interactive='{ "namespace": "myPlugin" }' | ||
data-wp-context='{ "isOpen": false }' | ||
data-wp-watch="callbacks.logIsOpen" | ||
> | ||
<button | ||
data-wp-on--click="actions.myNamespace.toggle" | ||
data-wp-bind--aria-expanded="context.myNamespace.isOpen" | ||
data-wp-on--click="actions.toggle" | ||
data-wp-bind--aria-expanded="context.isOpen" | ||
aria-controls="p-1" | ||
@@ -66,3 +63,3 @@ > | ||
<p id="p-1" data-bind--hidden="!context.myNamespace.isOpen"> | ||
<p id="p-1" data-bind--hidden="!context.isOpen"> | ||
This element is now visible! | ||
@@ -73,5 +70,2 @@ </p> | ||
> **Note** | ||
> The use of Namespaces to define the context, state or any other elements of the store is highly recommended to avoid possible collision with other elements with the same name. In the following examples we have not used namespaces for the sake of simplicity. | ||
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). | ||
@@ -81,12 +75,12 @@ | ||
With directives we can manage directly in the DOM behavior related to things such as side effects, state, event handlers, attributes or content. | ||
With directives, we can directly manage behavior 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 `wp-interactive` directive "activates" the interactivity for the DOM element and its children through the Interactivity API (directives and store). It includes a namespace to reference a specific store. | ||
```html | ||
<!-- Let's make this element and its children interactive --> | ||
<!-- Let's make this element and its children interactive and set the namespace --> | ||
<div | ||
data-wp-interactive | ||
data-wp-interactive='{ "namespace": "myPlugin" }' | ||
data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }' | ||
@@ -102,15 +96,15 @@ > | ||
> **Note** | ||
> The use of `wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `wp-interactive` has not been added for the sake of simplicity. | ||
> 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. | ||
#### `wp-context` | ||
It provides **local** state available to a specific HTML node and its children. | ||
It provides a **local** state available to a specific HTML node and its children. | ||
The `wp-context` directive accepts a stringified JSON as value. | ||
The `wp-context` directive accepts a stringified JSON as a value. | ||
_Example of `wp-context` directive_ | ||
```php | ||
//render.php | ||
<div data-wp-context='{ {"post": { "id": <?php echo $post->ID; ?> } } ' > | ||
<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }' > | ||
<button data-wp-on--click="actions.logId" > | ||
@@ -126,6 +120,7 @@ Click Me! | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
logId: ( { context } ) => { | ||
console.log( context.post.id ); | ||
logId: () => { | ||
const { post } = getContext(); | ||
console.log( post.id ); | ||
}, | ||
@@ -139,3 +134,3 @@ }, | ||
Different contexts can be defined at different levels and deeper levels will merge their own context with any parent one: | ||
Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one: | ||
@@ -164,2 +159,3 @@ ```html | ||
_Example of `wp-bind` directive_ | ||
```html | ||
@@ -181,2 +177,3 @@ <li data-wp-context='{ "isMenuOpen": false }'> | ||
``` | ||
<details> | ||
@@ -186,8 +183,9 @@ <summary><em>See store used with the directive above</em></summary> | ||
```js | ||
store( { | ||
actions: { | ||
toggleMenu: ( { context } ) => { | ||
store( "myPlugin", { | ||
actions: { | ||
toggleMenu: () => { | ||
const context = getContext(); | ||
context.isMenuOpen = !context.isMenuOpen; | ||
}, | ||
}, | ||
}, | ||
} ); | ||
@@ -200,11 +198,13 @@ ``` | ||
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 on getting the final value of the directive (inside the callback or the expression passed as reference). | ||
- 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 callback receives the attribute name: `attribute`. | ||
- 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: | ||
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: `<div attribute>`. | ||
@@ -222,6 +222,7 @@ - If the value is `false`, the attribute is removed: `<div>`. | ||
_Example of `wp-class` directive_ | ||
```php | ||
```html | ||
<div> | ||
<li | ||
data-wp-context='{ "isSelected": false } ' | ||
data-wp-context='{ "isSelected": false }' | ||
data-wp-on--click="actions.toggleSelection" | ||
@@ -233,3 +234,3 @@ data-wp-class--selected="context.isSelected" | ||
<li | ||
data-wp-context='{ "isSelected": false } ' | ||
data-wp-context='{ "isSelected": false }' | ||
data-wp-on--click="actions.toggleSelection" | ||
@@ -247,5 +248,6 @@ data-wp-class--selected="context.isSelected" | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
toggleSelection: ( { context } ) => { | ||
toggleSelection: () => { | ||
const context = getContext(); | ||
context.isSelected = !context.isSelected | ||
@@ -261,5 +263,6 @@ } | ||
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 on getting the final value of the directive (inside the callback or the expression passed as reference). | ||
- 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`. | ||
@@ -269,3 +272,2 @@ | ||
#### `wp-style` | ||
@@ -278,2 +280,3 @@ | ||
_Example of `wp-style` directive_ | ||
```html | ||
@@ -291,5 +294,6 @@ <div data-wp-context='{ "color": "red" }' > | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
toggleContextColor: ( { context } ) => { | ||
toggleContextColor: () => { | ||
const context = getContext(); | ||
context.color = context.color === 'red' ? 'blue' : 'red'; | ||
@@ -305,11 +309,13 @@ }, | ||
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 on getting the final value of the directive (inside the callback or the expression passed as reference). | ||
- 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: `<div>`. | ||
- If the value is a string, the attribute is added with its value assigned: `<div style="css-property: value;">`. | ||
- If the value is `false`, the style attribute is removed: `<div>`. | ||
- If the value is a string, the attribute is added with its value assigned: `<div style="css-property: value;">`. | ||
#### `wp-text` | ||
@@ -332,5 +338,6 @@ | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
toggleContextText: ( { context } ) => { | ||
toggleContextText: () => { | ||
const context = getContext(); | ||
context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1'; | ||
@@ -346,5 +353,6 @@ }, | ||
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 on getting the final value of the directive (inside the callback or the expression passed as reference). | ||
- 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: `<div>value</div>`. | ||
@@ -359,2 +367,3 @@ | ||
_Example of `wp-on` directive_ | ||
```php | ||
@@ -370,5 +379,7 @@ <button data-wp-on--click="actions.logTime" > | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
logTime: () => console.log( new Date() ), | ||
logTime: ( event ) => { | ||
console.log( new Date() ) | ||
}, | ||
}, | ||
@@ -383,16 +394,16 @@ } ); | ||
The callback passed as reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`) and the returned value by this callback is ignored. | ||
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-watch` | ||
#### `wp-effect` | ||
It runs a callback **when the node is created and runs it again when the state or context changes**. | ||
You can attach several effects to the same DOM element by using the syntax `data-wp-effect--[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-effect` directives of that DOM element._ | ||
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-effect` directive_ | ||
_Example of `wp-watch` directive_ | ||
```html | ||
<div | ||
data-wp-context='{ "counter": 0 }' | ||
data-wp-effect="effects.logCounter" | ||
data-wp-watch="callbacks.logCounter" | ||
> | ||
@@ -409,14 +420,19 @@ <p>Counter: <span data-wp-text="context.counter"></span></p> | ||
```js | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
increaseCounter: ({ context }) => { | ||
increaseCounter: () => { | ||
const context = getContext(); | ||
context.counter++; | ||
}, | ||
decreaseCounter: ({ context }) => { | ||
decreaseCounter: () => { | ||
const context = getContext(); | ||
context.counter--; | ||
}, | ||
} | ||
effects: { | ||
logCounter: ({ context }) => console.log("Counter is " + context.counter + " at " + new Date() ), | ||
}, | ||
callbacks: { | ||
logCounter: () => { | ||
const { counter } = getContext(); | ||
console.log("Counter is " + counter + " at " + new Date() ); | ||
}, | ||
}, | ||
} ); | ||
@@ -428,14 +444,16 @@ ``` | ||
The `wp-effect` 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 is executed: | ||
The `wp-effect` 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. | ||
- When the element is created. | ||
- Each time that any of the properties of the `state` or `context` used inside the callback changes. | ||
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. | ||
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` | ||
@@ -448,4 +466,5 @@ | ||
_Example of `data-wp-init` directive_ | ||
```html | ||
<div data-wp-init="effects.logTimeInit"> | ||
<div data-wp-init="callbacks.logTimeInit"> | ||
<p>Hi!</> | ||
@@ -456,6 +475,7 @@ </div> | ||
_Example of several `wp-init` directives on the same DOM element_ | ||
```html | ||
<form | ||
data-wp-init--log="effects.logTimeInit" | ||
data-wp-init--focus="effects.focusFirstElement" | ||
data-wp-init--log="callbacks.logTimeInit" | ||
data-wp-init--focus="callbacks.focusFirstElement" | ||
> | ||
@@ -470,7 +490,9 @@ <input type="text"> | ||
```js | ||
store( { | ||
effects: { | ||
store( "myPlugin", { | ||
callbacks: { | ||
logTimeInit: () => console.log( `Init at ` + new Date() ), | ||
focusFirstElement: ( { ref } ) => | ||
focusFirstElement: () => { | ||
const { ref } = getElement(); | ||
ref.querySelector( 'input:first-child' ).focus(), | ||
}, | ||
}, | ||
@@ -483,3 +505,2 @@ } ); | ||
The `wp-init` can return a function. If it does, the returned function will run when the element is removed from the DOM. | ||
@@ -489,7 +510,6 @@ | ||
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 `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: | ||
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 | ||
@@ -515,20 +535,21 @@ <ul> | ||
The value assigned to a directive is a string pointing to a specific state, selector, action, or effect. *Using a Namespace is highly recommended* to define these elements of the store. | ||
The value assigned to a directive is a string pointing to a specific state, action, or side effect. | ||
In the following example we use the namespace `wpmovies` (plugin name is usually a good namespace name) to define the `isPlaying` selector. | ||
In the following example, we use a getter to define the `state.isPlaying` derived value. | ||
```js | ||
store( { | ||
selectors: { | ||
wpmovies: { | ||
isPlaying: ( { state } ) => state.wpmovies.currentVideo !== '', | ||
}, | ||
}, | ||
const { state } = store( "myPlugin", { | ||
state: { | ||
currentVideo: '', | ||
get isPlaying() { | ||
return state.currentVideo !== ''; | ||
} | ||
}, | ||
} ); | ||
``` | ||
And then, we use the string value `"selectors.wpmovies.isPlaying"` to assign the result of this selector to `data-bind--hidden`. | ||
And then, we use the string value `"state.isPlaying"` to assign the result of this selector to `data-bind--hidden`. | ||
```php | ||
<div data-bind--hidden="!selectors.wpmovies.isPlaying" ... > | ||
```html | ||
<div data-bind--hidden="!state.isPlaying" ... > | ||
<iframe ...></iframe> | ||
@@ -538,13 +559,22 @@ </div> | ||
These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element (action, effect...) refers to without any additional configuration. | ||
These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element refers to, without any additional configuration. | ||
Note that, by default, references point to properties in the current namespace, which is the one specified by the closest ancestor with a `data-wp-interactive` attribute. If you need to access a property from a different namespace, you can explicitly set the namespace where the property we want to access is defined. The syntax is `namespace::reference`, replacing `namespace` with the appropriate value. | ||
In the example below, we get `state.isPlaying` from `otherPlugin` instead of `myPlugin`: | ||
```html | ||
<div data-wp-interactive='{ "namespace": "myPlugin" }'> | ||
<div data-bind--hidden="otherPlugin::!state.isPlaying" ... > | ||
<iframe ...></iframe> | ||
</div> | ||
</div> | ||
``` | ||
## The store | ||
The store is used to create the logic (actions, effects…) linked to the directives and the data used inside that logic (state, selectors…). | ||
The store is used to create the logic (actions, side effects…) linked to the directives and the data used inside that logic (state, derived state…). | ||
**The store is usually created in the `view.js` file of each block**, although it can be initialized from the `render.php` of the block. | ||
**The store is usually created in the `view.js` file of each block**, although the state can be initialized from the `render.php` of the block. | ||
The store contains the reactive state and the actions and effects that modify it. | ||
### Elements of the store | ||
@@ -554,6 +584,7 @@ | ||
Defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data: | ||
- **Global state**: It is defined using the `store()` function, and the data is available to all the HTML nodes of the page. It can be accessed using the `state` property. | ||
- **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `context` property. | ||
It defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data: | ||
- **Global state**: It is defined using the `store()` function with the `state` property, and the data is available to all the HTML nodes of the page. | ||
- **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `getContext` function inside of an action, derived state or side effect. | ||
```html | ||
@@ -572,3 +603,3 @@ <div data-wp-context='{ "someText": "Hello World!" }'> | ||
```js | ||
store( { | ||
const { state } = store( "myPlugin", { | ||
state: { | ||
@@ -578,4 +609,6 @@ someText: "Hello Universe!" | ||
actions: { | ||
someAction: ({ state, context }) => { | ||
someAction: () => { | ||
state.someText // Access or modify global state - "Hello Universe!" | ||
const context = getContext(); | ||
context.someText // Access or modify local state (context) - "Hello World!" | ||
@@ -591,69 +624,97 @@ }, | ||
#### Effects | ||
#### Side Effects | ||
Automatically react to state changes. Usually triggered by `data-wp-effect` or `data-wp-init` directives. | ||
Automatically react to state changes. Usually triggered by `data-wp-watch` or `data-wp-init` directives. | ||
#### Selectors | ||
#### Derived state | ||
Also known as _derived state_, returns a computed version of the state. They can access both `state` and `context`. | ||
They return a computed version of the state. They can access both `state` and `context`. | ||
```js | ||
// view.js | ||
store( { | ||
state: { | ||
amount: 34, | ||
defaultCurrency: 'EUR', | ||
currencyExchange: { | ||
USD: 1.1, | ||
GBP: 0.85, | ||
}, | ||
}, | ||
selectors: { | ||
amountInUSD: ( { state } ) => | ||
state.currencyExchange[ 'USD' ] * state.amount, | ||
amountInGBP: ( { state } ) => | ||
state.currencyExchange[ 'GBP' ] * state.amount, | ||
}, | ||
const { state } = store( "myPlugin", { | ||
state: { | ||
amount: 34, | ||
defaultCurrency: 'EUR', | ||
currencyExchange: { | ||
USD: 1.1, | ||
GBP: 0.85, | ||
}, | ||
get amountInUSD() { | ||
return state.currencyExchange[ 'USD' ] * state.amount, | ||
}, | ||
get amountInGBP() { | ||
return state.currencyExchange[ 'GBP' ] * state.amount, | ||
}, | ||
}, | ||
} ); | ||
``` | ||
### Arguments passed to callbacks | ||
### Accessing data in callbacks | ||
When a directive is evaluated, the reference callback receives an object with: | ||
- The **`store`** containing all the store properties, like `state`, `selectors`, `actions` or `effects` | ||
- The **context** (an object containing the context defined in all the `wp-context` ancestors). | ||
- The reference to the DOM element on which the directive was defined (a `ref`). | ||
- Other properties relevant to the directive. For example, the `data-wp-on--click` directive also receives the instance of the [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) triggered by the user. | ||
The **`store`** contains all the store properties, like `state`, `actions` or `callbacks`. They are returned by the `store()` call, so you can access them by destructuring it: | ||
_Example of action making use of all values received when it's triggered_ | ||
```js | ||
// view.js | ||
store( { | ||
state: { | ||
theme: false, | ||
}, | ||
actions: { | ||
toggle: ( { state, context, ref, event, className } ) => { | ||
console.log( state ); | ||
// `{ "theme": false }` | ||
console.log( context ); | ||
// `{ "isOpen": true }` | ||
console.log( ref ); | ||
// The DOM element | ||
console.log( event ); | ||
// The Event object if using the `data-wp-on` | ||
console.log( className ); | ||
// The class name if using the `data-wp-class` | ||
}, | ||
}, | ||
const { state, actions } = store( "myPlugin", { | ||
// ... | ||
} ); | ||
``` | ||
The `store()` function can be called multiple times and all the store parts will be merged together: | ||
```js | ||
store( "myPlugin", { | ||
state: { | ||
someValue: 1, | ||
} | ||
} ); | ||
const { state } = store( "myPlugin", { | ||
actions: { | ||
someAction() { | ||
state.someValue // = 1 | ||
} | ||
} | ||
} ); | ||
``` | ||
> **Note** | ||
> All `store()` calls with the same namespace return the same references, i.e., the same `state`, `actions`, etc., containing the result of merging all the store parts passed. | ||
- To access the context inside an action, derived state, or side effect, you can use the `getContext` function. | ||
- To access the reference, you can use the `getElement` function. | ||
```js | ||
const { state } = store( "myPlugin", { | ||
state: { | ||
get someDerivedValue() { | ||
const context = getContext(); | ||
const { ref } = getElement(); | ||
// ... | ||
} | ||
}, | ||
actions: { | ||
someAction() { | ||
const context = getContext(); | ||
const { ref } = getElement(); | ||
// ... | ||
} | ||
}, | ||
callbacks: { | ||
someEffect() { | ||
const context = getContext(); | ||
const { ref } = getElement(); | ||
// ... | ||
} | ||
} | ||
} ); | ||
``` | ||
This approach enables some functionalities that make directives flexible and powerful: | ||
- Actions and effects can read and modify the state and the context. | ||
- Actions and side effects can read and modify the state and the context. | ||
- Actions and state in blocks can be accessed by other blocks. | ||
- Actions and effects can do anything a regular JavaScript function can do, like access the DOM or make API requests. | ||
- Effects automatically react to state changes. | ||
- Actions and side effects can do anything a regular JavaScript function can do, like access the DOM or make API requests. | ||
- Side effects automatically react to state changes. | ||
@@ -664,20 +725,22 @@ ### Setting the store | ||
*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, effects or selectors. | ||
*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, side effects or derived state. | ||
`store` method used to set the store in javascript can be imported from `@wordpress/interactivity`. | ||
The `store` method used to set the store in javascript can be imported from `@wordpress/interactivity`. | ||
```js | ||
// store | ||
import { store } from '@wordpress/interactivity'; | ||
import { store, getContext } from '@wordpress/interactivity'; | ||
store( { | ||
store( "myPlugin", { | ||
actions: { | ||
toggle: ( { context } ) => { | ||
toggle: () => { | ||
const context = getContext(); | ||
context.isOpen = !context.isOpen; | ||
}, | ||
}, | ||
effects: { | ||
logIsOpen: ( { context } ) => { | ||
callbacks: { | ||
logIsOpen: () => { | ||
const { isOpen } = getContext(); | ||
// Log the value of `isOpen` each time it changes. | ||
console.log( `Is open: ${ context.isOpen }` ); | ||
console.log( `Is open: ${ isOpen }` ); | ||
} | ||
@@ -690,9 +753,11 @@ }, | ||
The store can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1). | ||
> **Note** | ||
> We will rename `wp_store` to `wp_initial_state` in a future version. | ||
The store defined on the server with `wp_store()` gets merged with the stores defined in the view.js files. | ||
The state can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1). | ||
The state defined on the server with `wp_store()` gets merged with the stores defined in the view.js files. | ||
The `wp_store` function receives an [associative array](https://www.php.net/manual/en/language.types.array.php) as a parameter. | ||
_Example of store initialized from the server with a `state` = `{ someValue: 123 }`_ | ||
@@ -703,6 +768,4 @@ | ||
wp_store( array( | ||
'state' => array( | ||
'myNamespace' => array( | ||
'someValue' = 123 | ||
) | ||
'myPlugin' => array( | ||
'someValue' = 123 | ||
) | ||
@@ -712,3 +775,3 @@ ); | ||
Initializing the store in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state: | ||
Initializing the state in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state: | ||
@@ -719,8 +782,6 @@ ```php | ||
array( | ||
"state" => array( | ||
"favoriteMovies" => array( | ||
"1" => array( | ||
"id" => "123-abc", | ||
"movieName" => __("someMovieName", "textdomain") | ||
), | ||
"favoriteMovies" => array( | ||
"1" => array( | ||
"id" => "123-abc", | ||
"movieName" => __("someMovieName", "textdomain") | ||
), | ||
@@ -732,29 +793,28 @@ ), | ||
### Store options | ||
### Private stores | ||
The `store` function accepts an object as a second argument with the following optional properties: | ||
A given store namespace can be marked as private, thus preventing its content to be accessed from other namespaces. The mechanism to do so is by adding a `lock` option to the `store()` call, like shown in the example below. This way, further executions of `store()` with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first `store()` call. This is specially useful for developers that want to hide part of their plugin stores so it doesn't become accessible for extenders. | ||
#### `afterLoad` | ||
Callback to be executed after the Interactivity API has been set up and the store is ready. It receives the global store as argument. | ||
```js | ||
// view.js | ||
store( | ||
{ | ||
state: { | ||
cart: [], | ||
}, | ||
}, | ||
{ | ||
afterLoad: async ( { state } ) => { | ||
// Let's consider `clientId` is added | ||
// during server-side rendering. | ||
state.cart = await getCartData( state.clientId ); | ||
}, | ||
} | ||
const { state } = store( | ||
"myPlugin/private", | ||
{ state: { messages: [ "private message" ] } }, | ||
{ lock: true } | ||
); | ||
// The following call throws an Error! | ||
store( "myPlugin/private", { /* store part */ } ); | ||
``` | ||
There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the `lock` value. Such a string can then be used in subsequent `store()` calls to the same namespace to unlock its content. Only the code knowing the string lock will be able to unlock the protected store namespaced. This is useful for complex stores defined in multiple JS modules. | ||
```js | ||
const { state } = store( | ||
"myPlugin/private", | ||
{ state: { messages: [ "private message" ] } }, | ||
{ lock: PRIVATE_LOCK } | ||
); | ||
// The following call works as expected. | ||
store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } ); | ||
``` |
{ | ||
"name": "@wordpress/interactivity", | ||
"version": "3.0.0", | ||
"version": "3.0.1", | ||
"description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", | ||
@@ -36,3 +36,3 @@ "author": "The WordPress Contributors", | ||
}, | ||
"gitHead": "d98dff8ea96f29cfea045bf964269f46f040d539" | ||
"gitHead": "12b897d7feff1cb00ddbf9016b62c1177d9c0081" | ||
} |
@@ -62,4 +62,14 @@ /** | ||
// Prefetch a page. We store the promise to avoid triggering a second fetch for | ||
// a page if a fetching has already started. | ||
/** | ||
* Prefetchs the page with the passed URL. | ||
* | ||
* The function normalizes the URL and stores internally the fetch promise, to | ||
* avoid triggering a second fetch for an ongoing request. | ||
* | ||
* @param {string} url The page URL. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] Force fetching the URL again. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
*/ | ||
export const prefetch = ( url, options = {} ) => { | ||
@@ -88,3 +98,22 @@ url = cleanUrl( url ); | ||
// Navigate to a new page. | ||
/** | ||
* Navigates to the specified page. | ||
* | ||
* This function normalizes the passed href, fetchs the page HTML if needed, and | ||
* updates any interactive regions whose contents have changed. It also creates | ||
* a new entry in the browser session history. | ||
* | ||
* @param {string} href The page href. | ||
* @param {Object} [options] Options object. | ||
* @param {boolean} [options.force] If true, it forces re-fetching the URL. | ||
* @param {string} [options.html] HTML string to be used instead of fetching | ||
* the requested URL. | ||
* @param {boolean} [options.replace] If true, it replaces the current entry in | ||
* the browser session history. | ||
* @param {number} [options.timeout] Time until the navigation is aborted, in | ||
* milliseconds. Default is 10000. | ||
* | ||
* @return {Promise} Promise that resolves once the navigation is completed or | ||
* aborted. | ||
*/ | ||
export const navigate = async ( href, options = {} ) => { | ||
@@ -91,0 +120,0 @@ const url = cleanUrl( href ); |
@@ -167,35 +167,57 @@ /** | ||
}; | ||
interface StoreOptions { | ||
/** | ||
* Property to block/unblock private store namespaces. | ||
* | ||
* If the passed value is `true`, it blocks the given namespace, making it | ||
* accessible only trough the returned variables of the `store()` call. In | ||
* the case a lock string is passed, it also blocks the namespace, but can | ||
* be unblocked for other `store()` calls using the same lock string. | ||
* | ||
* @example | ||
* ``` | ||
* // The store can only be accessed where the `state` const can. | ||
* const { state } = store( 'myblock/private', { ... }, { lock: true } ); | ||
* ``` | ||
* | ||
* @example | ||
* ``` | ||
* // Other modules knowing `SECRET_LOCK_STRING` can access the namespace. | ||
* const { state } = store( | ||
* 'myblock/private', | ||
* { ... }, | ||
* { lock: 'SECRET_LOCK_STRING' } | ||
* ); | ||
* ``` | ||
*/ | ||
lock?: boolean | string; | ||
} | ||
/** | ||
* @typedef StoreProps Properties object passed to `store`. | ||
* @property {Object} state State to be added to the global store. All the | ||
* properties included here become reactive. | ||
*/ | ||
const universalUnlock = | ||
'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
/** | ||
* @typedef StoreOptions Options object. | ||
*/ | ||
/** | ||
* Extends the Interactivity API global store with the passed properties. | ||
* Extends the Interactivity API global store adding the passed properties to | ||
* the given namespace. It also returns stable references to the namespace | ||
* content. | ||
* | ||
* These props typically consist of `state`, which is reactive, and other | ||
* properties like `selectors`, `actions`, `effects`, etc. which can store | ||
* callbacks and derived state. These props can then be referenced by any | ||
* directive to make the HTML interactive. | ||
* These props typically consist of `state`, which is the reactive part of the | ||
* store ― which means that any directive referencing a state property will be | ||
* re-rendered anytime it changes ― and function properties like `actions` and | ||
* `callbacks`, mostly used for event handlers. These props can then be | ||
* referenced by any directive to make the HTML interactive. | ||
* | ||
* @example | ||
* ```js | ||
* store({ | ||
* const { state } = store( 'counter', { | ||
* state: { | ||
* counter: { value: 0 }, | ||
* value: 0, | ||
* get double() { return state.value * 2; }, | ||
* }, | ||
* actions: { | ||
* counter: { | ||
* increment: ({ state }) => { | ||
* state.counter.value += 1; | ||
* }, | ||
* increment() { | ||
* state.value += 1; | ||
* }, | ||
* }, | ||
* }); | ||
* } ); | ||
* ``` | ||
@@ -207,6 +229,6 @@ * | ||
* ```html | ||
* <div data-wp-interactive> | ||
* <div data-wp-interactive='{ "namespace": "counter" }'> | ||
* <button | ||
* data-wp-text="state.counter.value" | ||
* data-wp-on--click="actions.counter.increment" | ||
* data-wp-text="state.double" | ||
* data-wp-on--click="actions.increment" | ||
* > | ||
@@ -217,14 +239,8 @@ * 0 | ||
* ``` | ||
* @param namespace The store namespace to interact with. | ||
* @param storePart Properties to add to the store namespace. | ||
* @param options Options for the given namespace. | ||
* | ||
* @param {StoreProps} properties Properties to be added to the global store. | ||
* @param {StoreOptions} [options] Options passed to the `store` call. | ||
* @return A reference to the namespace content. | ||
*/ | ||
interface StoreOptions { | ||
lock?: boolean | string; | ||
} | ||
const universalUnlock = | ||
'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
export function store< S extends object = {} >( | ||
@@ -235,2 +251,3 @@ namespace: string, | ||
): S; | ||
export function store< T extends object >( | ||
@@ -237,0 +254,0 @@ namespace: string, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
920110
4663