UNPKG

@wordpress/interactivity

Version:

Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.

190 lines (181 loc) 6.23 kB
/** * Internal dependencies */ import { proxifyState, proxifyStore, deepMerge } from './proxies'; /** * External dependencies */ import { getNamespace } from './namespaces'; import { isPlainObject } from './utils'; export const stores = new Map(); const rawStores = new Map(); const storeLocks = new Map(); const storeConfigs = new Map(); const serverStates = new Map(); /** * Gets the defined config for the store with the passed namespace. * * @param namespace Store's namespace from which to retrieve the config. * @return Defined config for the given namespace. */ export const getConfig = namespace => storeConfigs.get(namespace || getNamespace()) || {}; /** * Gets the part of the state defined and updated from the server. * * The object returned is read-only, and includes the state defined in PHP with * `wp_interactivity_state()`. When using `actions.navigate()`, this object is * updated to reflect the changes in its properties, without affecting the state * returned by `store()`. Directives can subscribe to those changes to update * the state if needed. * * @example * ```js * const { state } = store('myStore', { * callbacks: { * updateServerState() { * const serverState = getServerState(); * // Override some property with the new value that came from the server. * state.overridableProp = serverState.overridableProp; * }, * }, * }); * ``` * * @param namespace Store's namespace from which to retrieve the server state. * @return The server state for the given namespace. */ export const getServerState = namespace => { const ns = namespace || getNamespace(); if (!serverStates.has(ns)) { serverStates.set(ns, proxifyState(ns, {}, { readOnly: true })); } return serverStates.get(ns); }; export const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; /** * 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 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 * const { state } = store( 'counter', { * state: { * value: 0, * get double() { return state.value * 2; }, * }, * actions: { * increment() { * state.value += 1; * }, * }, * } ); * ``` * * The code from the example above allows blocks to subscribe and interact with * the store by using directives in the HTML, e.g.: * * ```html * <div data-wp-interactive="counter"> * <button * data-wp-text="state.double" * data-wp-on--click="actions.increment" * > * 0 * </button> * </div> * ``` * @param namespace The store namespace to interact with. * @param storePart Properties to add to the store namespace. * @param options Options for the given namespace. * * @return A reference to the namespace content. */ // Overload for when the types are inferred. // Overload for when types are passed via generics and they contain state. // Overload for when types are passed via generics and they don't contain state. // Overload for when types are divided into multiple parts. export function store(namespace, { state = {}, ...block } = {}, { lock = false } = {}) { if (!stores.has(namespace)) { // Lock the store if the passed lock is different from the universal // unlock. Once the lock is set (either false, true, or a given string), // it cannot change. if (lock !== universalUnlock) { storeLocks.set(namespace, lock); } const rawStore = { state: proxifyState(namespace, isPlainObject(state) ? state : {}), ...block }; const proxifiedStore = proxifyStore(namespace, rawStore); rawStores.set(namespace, rawStore); stores.set(namespace, proxifiedStore); } else { // Lock the store if it wasn't locked yet and the passed lock is // different from the universal unlock. If no lock is given, the store // will be public and won't accept any lock from now on. if (lock !== universalUnlock && !storeLocks.has(namespace)) { storeLocks.set(namespace, lock); } else { const storeLock = storeLocks.get(namespace); const isLockValid = lock === universalUnlock || lock !== true && lock === storeLock; if (!isLockValid) { if (!storeLock) { throw Error('Cannot lock a public store'); } else { throw Error('Cannot unlock a private store with an invalid lock code'); } } } const target = rawStores.get(namespace); deepMerge(target, block); deepMerge(target.state, state); } return stores.get(namespace); } export const parseServerData = (dom = document) => { var _dom$getElementById; const jsonDataScriptTag = // Preferred Script Module data passing form (_dom$getElementById = dom.getElementById('wp-script-module-data-@wordpress/interactivity')) !== null && _dom$getElementById !== void 0 ? _dom$getElementById : // Legacy form dom.getElementById('wp-interactivity-data'); if (jsonDataScriptTag?.textContent) { try { return JSON.parse(jsonDataScriptTag.textContent); } catch {} } return {}; }; export const populateServerData = data => { if (isPlainObject(data?.state)) { Object.entries(data.state).forEach(([namespace, state]) => { const st = store(namespace, {}, { lock: universalUnlock }); deepMerge(st.state, state, false); deepMerge(getServerState(namespace), state); }); } if (isPlainObject(data?.config)) { Object.entries(data.config).forEach(([namespace, config]) => { storeConfigs.set(namespace, config); }); } }; // Parse and populate the initial state and config. const data = parseServerData(); populateServerData(data); //# sourceMappingURL=store.js.map