@wordpress/interactivity
Advanced tools
Comparing version 6.8.1 to 6.8.2
@@ -134,5 +134,11 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair | ||
}) => suffix === 'default'); | ||
const inheritedValue = useContext(inheritedContext); | ||
const { | ||
client: inheritedClient, | ||
server: inheritedServer | ||
} = useContext(inheritedContext); | ||
const ns = defaultEntry.namespace; | ||
const currentValue = useRef(proxifyState(ns, {})); | ||
const client = useRef(proxifyState(ns, {})); | ||
const server = useRef(proxifyState(ns, {}, { | ||
readOnly: true | ||
})); | ||
@@ -142,3 +148,8 @@ // No change should be made if `defaultEntry` does not exist. | ||
const result = { | ||
...inheritedValue | ||
client: { | ||
...inheritedClient | ||
}, | ||
server: { | ||
...inheritedServer | ||
} | ||
}; | ||
@@ -154,7 +165,9 @@ if (defaultEntry) { | ||
} | ||
deepMerge(currentValue.current, deepClone(value), false); | ||
result[namespace] = proxifyContext(currentValue.current, inheritedValue[namespace]); | ||
deepMerge(client.current, deepClone(value), false); | ||
deepMerge(server.current, deepClone(value)); | ||
result.client[namespace] = proxifyContext(client.current, inheritedClient[namespace]); | ||
result.server[namespace] = proxifyContext(server.current, inheritedServer[namespace]); | ||
} | ||
return result; | ||
}, [defaultEntry, inheritedValue]); | ||
}, [defaultEntry, inheritedClient, inheritedServer]); | ||
return createElement(Provider, { | ||
@@ -547,13 +560,19 @@ value: contextStack | ||
const itemProp = suffix === 'default' ? 'item' : kebabToCamelCase(suffix); | ||
const itemContext = proxifyContext(proxifyState(namespace, {}), inheritedValue[namespace]); | ||
const itemContext = proxifyContext(proxifyState(namespace, {}), inheritedValue.client[namespace]); | ||
const mergedContext = { | ||
...inheritedValue, | ||
[namespace]: itemContext | ||
client: { | ||
...inheritedValue.client, | ||
[namespace]: itemContext | ||
}, | ||
server: { | ||
...inheritedValue.server | ||
} | ||
}; | ||
// Set the item after proxifying the context. | ||
mergedContext[namespace][itemProp] = item; | ||
mergedContext.client[namespace][itemProp] = item; | ||
const scope = { | ||
...getScope(), | ||
context: mergedContext | ||
context: mergedContext.client, | ||
serverContext: mergedContext.server | ||
}; | ||
@@ -560,0 +579,0 @@ const key = eachKey ? getEvaluate({ |
@@ -16,3 +16,6 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair | ||
// Main context. | ||
const context = createContext({}); | ||
const context = createContext({ | ||
client: {}, | ||
server: {} | ||
}); | ||
@@ -172,3 +175,8 @@ // WordPress Directives. | ||
}), []); | ||
scope.context = useContext(context); | ||
const { | ||
client, | ||
server | ||
} = useContext(context); | ||
scope.context = client; | ||
scope.serverContext = server; | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
@@ -175,0 +183,0 @@ scope.ref = previousScope?.ref || useRef(null); |
@@ -18,4 +18,4 @@ /** | ||
import { proxifyState } from './proxies'; | ||
export { store, getConfig } from './store'; | ||
export { getContext, getElement } from './scopes'; | ||
export { store, getConfig, getServerState } from './store'; | ||
export { getContext, getServerContext, getElement } from './scopes'; | ||
export { withScope, useWatch, useInit, useEffect, useLayoutEffect, useCallback, useMemo, splitTask } from './utils'; | ||
@@ -22,0 +22,0 @@ export { useState, useRef } from 'preact/hooks'; |
@@ -34,2 +34,3 @@ /** | ||
export const hasPropSignal = (proxy, key) => proxyToProps.has(proxy) && proxyToProps.get(proxy).has(key); | ||
const readOnlyProxies = new WeakSet(); | ||
@@ -66,3 +67,6 @@ /** | ||
} else { | ||
prop.setValue(shouldProxy(value) ? proxifyState(ns, value) : value); | ||
const readOnly = readOnlyProxies.has(proxy); | ||
prop.setValue(shouldProxy(value) ? proxifyState(ns, value, { | ||
readOnly | ||
}) : value); | ||
} | ||
@@ -125,2 +129,5 @@ } | ||
set(target, key, value, receiver) { | ||
if (readOnlyProxies.has(receiver)) { | ||
return false; | ||
} | ||
setNamespace(getNamespaceFromProxy(receiver)); | ||
@@ -134,2 +141,5 @@ try { | ||
defineProperty(target, key, desc) { | ||
if (readOnlyProxies.has(getProxyFromObject(target))) { | ||
return false; | ||
} | ||
const isNew = !(key in target); | ||
@@ -167,2 +177,5 @@ const result = Reflect.defineProperty(target, key, desc); | ||
deleteProperty(target, key) { | ||
if (readOnlyProxies.has(getProxyFromObject(target))) { | ||
return false; | ||
} | ||
const result = Reflect.deleteProperty(target, key); | ||
@@ -195,4 +208,6 @@ if (result) { | ||
* | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param options Options. | ||
* @param options.readOnly Read-only. | ||
* | ||
@@ -204,3 +219,9 @@ * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to | ||
*/ | ||
export const proxifyState = (namespace, obj) => createProxy(namespace, obj, stateHandlers); | ||
export const proxifyState = (namespace, obj, options) => { | ||
const proxy = createProxy(namespace, obj, stateHandlers); | ||
if (options?.readOnly) { | ||
readOnlyProxies.add(proxy); | ||
} | ||
return proxy; | ||
}; | ||
@@ -207,0 +228,0 @@ /** |
@@ -82,2 +82,41 @@ /** | ||
}; | ||
/** | ||
* Retrieves the part of the inherited context defined and updated from the | ||
* server. | ||
* | ||
* The object returned is read-only, and includes the context defined in PHP | ||
* with `wp_interactivity_data_wp_context()`, including the corresponding | ||
* inherited properties. When `actions.navigate()` is called, this object is | ||
* updated to reflect the changes in the new visited page, without affecting the | ||
* context returned by `getContext()`. Directives can subscribe to those changes | ||
* to update the context if needed. | ||
* | ||
* @example | ||
* ```js | ||
* store('...', { | ||
* callbacks: { | ||
* updateServerContext() { | ||
* const context = getContext(); | ||
* const serverContext = getServerContext(); | ||
* // Override some property with the new value that came from the server. | ||
* context.overridableProp = serverContext.overridableProp; | ||
* }, | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The server context content. | ||
*/ | ||
export const getServerContext = namespace => { | ||
const scope = getScope(); | ||
if (globalThis.SCRIPT_DEBUG) { | ||
if (!scope) { | ||
throw Error('Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.'); | ||
} | ||
} | ||
return scope.serverContext[namespace || getNamespace()]; | ||
}; | ||
//# sourceMappingURL=scopes.js.map |
@@ -14,2 +14,3 @@ /** | ||
const storeConfigs = new Map(); | ||
const serverStates = new Map(); | ||
@@ -23,2 +24,37 @@ /** | ||
export const getConfig = namespace => storeConfigs.get(namespace || getNamespace()) || {}; | ||
/** | ||
* Get 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 properites, 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.'; | ||
@@ -135,2 +171,3 @@ | ||
deepMerge(st.state, state, false); | ||
deepMerge(getServerState(namespace), state); | ||
}); | ||
@@ -137,0 +174,0 @@ } |
@@ -1,3 +0,3 @@ | ||
export { store, getConfig } from './store'; | ||
export { getContext, getElement } from './scopes'; | ||
export { store, getConfig, getServerState } from './store'; | ||
export { getContext, getServerContext, getElement } from './scopes'; | ||
export { withScope, useWatch, useInit, useEffect, useLayoutEffect, useCallback, useMemo, splitTask, } from './utils'; | ||
@@ -4,0 +4,0 @@ export { useState, useRef } from 'preact/hooks'; |
@@ -14,4 +14,6 @@ /** | ||
* | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param options Options. | ||
* @param options.readOnly Read-only. | ||
* | ||
@@ -23,3 +25,5 @@ * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to | ||
*/ | ||
export declare const proxifyState: <T extends object>(namespace: string, obj: T) => T; | ||
export declare const proxifyState: <T extends object>(namespace: string, obj: T, options?: { | ||
readOnly?: boolean; | ||
}) => T; | ||
/** | ||
@@ -26,0 +30,0 @@ * Reads the value of the specified property without subscribing to it. |
@@ -9,2 +9,3 @@ /** | ||
context: object; | ||
serverContext: object; | ||
ref: RefObject<HTMLElement>; | ||
@@ -37,2 +38,32 @@ attributes: createElement.JSX.HTMLAttributes; | ||
}>; | ||
/** | ||
* Retrieves the part of the inherited context defined and updated from the | ||
* server. | ||
* | ||
* The object returned is read-only, and includes the context defined in PHP | ||
* with `wp_interactivity_data_wp_context()`, including the corresponding | ||
* inherited properties. When `actions.navigate()` is called, this object is | ||
* updated to reflect the changes in the new visited page, without affecting the | ||
* context returned by `getContext()`. Directives can subscribe to those changes | ||
* to update the context if needed. | ||
* | ||
* @example | ||
* ```js | ||
* store('...', { | ||
* callbacks: { | ||
* updateServerContext() { | ||
* const context = getContext(); | ||
* const serverContext = getServerContext(); | ||
* // Override some property with the new value that came from the server. | ||
* context.overridableProp = serverContext.overridableProp; | ||
* }, | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The server context content. | ||
*/ | ||
export declare const getServerContext: <T extends object>(namespace?: string) => T; | ||
//# sourceMappingURL=scopes.d.ts.map |
@@ -9,2 +9,28 @@ export declare const stores: Map<any, any>; | ||
export declare const getConfig: (namespace?: string) => any; | ||
/** | ||
* Get 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 properites, 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 declare const getServerState: (namespace?: string) => any; | ||
interface StoreOptions { | ||
@@ -11,0 +37,0 @@ /** |
@@ -140,5 +140,11 @@ "use strict"; | ||
}) => suffix === 'default'); | ||
const inheritedValue = (0, _hooks.useContext)(inheritedContext); | ||
const { | ||
client: inheritedClient, | ||
server: inheritedServer | ||
} = (0, _hooks.useContext)(inheritedContext); | ||
const ns = defaultEntry.namespace; | ||
const currentValue = (0, _hooks.useRef)((0, _proxies.proxifyState)(ns, {})); | ||
const client = (0, _hooks.useRef)((0, _proxies.proxifyState)(ns, {})); | ||
const server = (0, _hooks.useRef)((0, _proxies.proxifyState)(ns, {}, { | ||
readOnly: true | ||
})); | ||
@@ -148,3 +154,8 @@ // No change should be made if `defaultEntry` does not exist. | ||
const result = { | ||
...inheritedValue | ||
client: { | ||
...inheritedClient | ||
}, | ||
server: { | ||
...inheritedServer | ||
} | ||
}; | ||
@@ -160,7 +171,9 @@ if (defaultEntry) { | ||
} | ||
(0, _proxies.deepMerge)(currentValue.current, deepClone(value), false); | ||
result[namespace] = (0, _proxies.proxifyContext)(currentValue.current, inheritedValue[namespace]); | ||
(0, _proxies.deepMerge)(client.current, deepClone(value), false); | ||
(0, _proxies.deepMerge)(server.current, deepClone(value)); | ||
result.client[namespace] = (0, _proxies.proxifyContext)(client.current, inheritedClient[namespace]); | ||
result.server[namespace] = (0, _proxies.proxifyContext)(server.current, inheritedServer[namespace]); | ||
} | ||
return result; | ||
}, [defaultEntry, inheritedValue]); | ||
}, [defaultEntry, inheritedClient, inheritedServer]); | ||
return (0, _preact.h)(Provider, { | ||
@@ -553,13 +566,19 @@ value: contextStack | ||
const itemProp = suffix === 'default' ? 'item' : (0, _utils.kebabToCamelCase)(suffix); | ||
const itemContext = (0, _proxies.proxifyContext)((0, _proxies.proxifyState)(namespace, {}), inheritedValue[namespace]); | ||
const itemContext = (0, _proxies.proxifyContext)((0, _proxies.proxifyState)(namespace, {}), inheritedValue.client[namespace]); | ||
const mergedContext = { | ||
...inheritedValue, | ||
[namespace]: itemContext | ||
client: { | ||
...inheritedValue.client, | ||
[namespace]: itemContext | ||
}, | ||
server: { | ||
...inheritedValue.server | ||
} | ||
}; | ||
// Set the item after proxifying the context. | ||
mergedContext[namespace][itemProp] = item; | ||
mergedContext.client[namespace][itemProp] = item; | ||
const scope = { | ||
...(0, _scopes.getScope)(), | ||
context: mergedContext | ||
context: mergedContext.client, | ||
serverContext: mergedContext.server | ||
}; | ||
@@ -566,0 +585,0 @@ const key = eachKey ? (0, _hooks2.getEvaluate)({ |
@@ -24,3 +24,6 @@ "use strict"; | ||
// Main context. | ||
const context = (0, _preact.createContext)({}); | ||
const context = (0, _preact.createContext)({ | ||
client: {}, | ||
server: {} | ||
}); | ||
@@ -182,3 +185,8 @@ // WordPress Directives. | ||
}), []); | ||
scope.context = (0, _hooks.useContext)(context); | ||
const { | ||
client, | ||
server | ||
} = (0, _hooks.useContext)(context); | ||
scope.context = client; | ||
scope.serverContext = server; | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
@@ -185,0 +193,0 @@ scope.ref = previousScope?.ref || (0, _hooks.useRef)(null); |
@@ -25,2 +25,14 @@ "use strict"; | ||
}); | ||
Object.defineProperty(exports, "getServerContext", { | ||
enumerable: true, | ||
get: function () { | ||
return _scopes.getServerContext; | ||
} | ||
}); | ||
Object.defineProperty(exports, "getServerState", { | ||
enumerable: true, | ||
get: function () { | ||
return _store.getServerState; | ||
} | ||
}); | ||
exports.privateApis = void 0; | ||
@@ -27,0 +39,0 @@ Object.defineProperty(exports, "splitTask", { |
@@ -40,2 +40,4 @@ "use strict"; | ||
const hasPropSignal = (proxy, key) => proxyToProps.has(proxy) && proxyToProps.get(proxy).has(key); | ||
exports.hasPropSignal = hasPropSignal; | ||
const readOnlyProxies = new WeakSet(); | ||
@@ -54,3 +56,2 @@ /** | ||
*/ | ||
exports.hasPropSignal = hasPropSignal; | ||
const getPropSignal = (proxy, key, initial) => { | ||
@@ -74,3 +75,6 @@ if (!proxyToProps.has(proxy)) { | ||
} else { | ||
prop.setValue((0, _registry.shouldProxy)(value) ? proxifyState(ns, value) : value); | ||
const readOnly = readOnlyProxies.has(proxy); | ||
prop.setValue((0, _registry.shouldProxy)(value) ? proxifyState(ns, value, { | ||
readOnly | ||
}) : value); | ||
} | ||
@@ -133,2 +137,5 @@ } | ||
set(target, key, value, receiver) { | ||
if (readOnlyProxies.has(receiver)) { | ||
return false; | ||
} | ||
(0, _namespaces.setNamespace)((0, _registry.getNamespaceFromProxy)(receiver)); | ||
@@ -142,2 +149,5 @@ try { | ||
defineProperty(target, key, desc) { | ||
if (readOnlyProxies.has((0, _registry.getProxyFromObject)(target))) { | ||
return false; | ||
} | ||
const isNew = !(key in target); | ||
@@ -175,2 +185,5 @@ const result = Reflect.defineProperty(target, key, desc); | ||
deleteProperty(target, key) { | ||
if (readOnlyProxies.has((0, _registry.getProxyFromObject)(target))) { | ||
return false; | ||
} | ||
const result = Reflect.deleteProperty(target, key); | ||
@@ -203,4 +216,6 @@ if (result) { | ||
* | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param options Options. | ||
* @param options.readOnly Read-only. | ||
* | ||
@@ -212,3 +227,9 @@ * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to | ||
*/ | ||
const proxifyState = (namespace, obj) => (0, _registry.createProxy)(namespace, obj, stateHandlers); | ||
const proxifyState = (namespace, obj, options) => { | ||
const proxy = (0, _registry.createProxy)(namespace, obj, stateHandlers); | ||
if (options?.readOnly) { | ||
readOnlyProxies.add(proxy); | ||
} | ||
return proxy; | ||
}; | ||
@@ -215,0 +236,0 @@ /** |
@@ -6,3 +6,3 @@ "use strict"; | ||
}); | ||
exports.setScope = exports.resetScope = exports.getScope = exports.getElement = exports.getContext = void 0; | ||
exports.setScope = exports.resetScope = exports.getServerContext = exports.getScope = exports.getElement = exports.getContext = void 0; | ||
var _namespaces = require("./namespaces"); | ||
@@ -94,3 +94,43 @@ /** | ||
}; | ||
/** | ||
* Retrieves the part of the inherited context defined and updated from the | ||
* server. | ||
* | ||
* The object returned is read-only, and includes the context defined in PHP | ||
* with `wp_interactivity_data_wp_context()`, including the corresponding | ||
* inherited properties. When `actions.navigate()` is called, this object is | ||
* updated to reflect the changes in the new visited page, without affecting the | ||
* context returned by `getContext()`. Directives can subscribe to those changes | ||
* to update the context if needed. | ||
* | ||
* @example | ||
* ```js | ||
* store('...', { | ||
* callbacks: { | ||
* updateServerContext() { | ||
* const context = getContext(); | ||
* const serverContext = getServerContext(); | ||
* // Override some property with the new value that came from the server. | ||
* context.overridableProp = serverContext.overridableProp; | ||
* }, | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The server context content. | ||
*/ | ||
exports.getElement = getElement; | ||
const getServerContext = namespace => { | ||
const scope = getScope(); | ||
if (globalThis.SCRIPT_DEBUG) { | ||
if (!scope) { | ||
throw Error('Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.'); | ||
} | ||
} | ||
return scope.serverContext[namespace || (0, _namespaces.getNamespace)()]; | ||
}; | ||
exports.getServerContext = getServerContext; | ||
//# sourceMappingURL=scopes.js.map |
@@ -6,3 +6,3 @@ "use strict"; | ||
}); | ||
exports.populateServerData = exports.parseServerData = exports.getConfig = void 0; | ||
exports.populateServerData = exports.parseServerData = exports.getServerState = exports.getConfig = void 0; | ||
exports.store = store; | ||
@@ -25,2 +25,3 @@ exports.universalUnlock = exports.stores = void 0; | ||
const storeConfigs = new Map(); | ||
const serverStates = new Map(); | ||
@@ -34,3 +35,39 @@ /** | ||
const getConfig = namespace => storeConfigs.get(namespace || (0, _namespaces.getNamespace)()) || {}; | ||
/** | ||
* Get 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 properites, 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. | ||
*/ | ||
exports.getConfig = getConfig; | ||
const getServerState = namespace => { | ||
const ns = namespace || (0, _namespaces.getNamespace)(); | ||
if (!serverStates.has(ns)) { | ||
serverStates.set(ns, (0, _proxies.proxifyState)(ns, {}, { | ||
readOnly: true | ||
})); | ||
} | ||
return serverStates.get(ns); | ||
}; | ||
exports.getServerState = getServerState; | ||
const universalUnlock = exports.universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; | ||
@@ -148,2 +185,3 @@ | ||
(0, _proxies.deepMerge)(st.state, state, false); | ||
(0, _proxies.deepMerge)(getServerState(namespace), state); | ||
}); | ||
@@ -150,0 +188,0 @@ } |
{ | ||
"name": "@wordpress/interactivity", | ||
"version": "6.8.1", | ||
"version": "6.8.2", | ||
"description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", | ||
@@ -40,3 +40,3 @@ "author": "The WordPress Contributors", | ||
}, | ||
"gitHead": "cf707c1f25a2716e310cc8f9afcc8554405c79ac" | ||
"gitHead": "3686e926862c54c6326fe97277e0a19f8bbfafdd" | ||
} |
@@ -19,4 +19,4 @@ /** | ||
export { store, getConfig } from './store'; | ||
export { getContext, getElement } from './scopes'; | ||
export { store, getConfig, getServerState } from './store'; | ||
export { getContext, getServerContext, getElement } from './scopes'; | ||
export { | ||
@@ -23,0 +23,0 @@ withScope, |
@@ -49,2 +49,4 @@ /** | ||
const readOnlyProxies = new WeakSet(); | ||
/** | ||
@@ -81,4 +83,7 @@ * Returns the {@link PropSignal | `PropSignal`} instance associated with the | ||
} else { | ||
const readOnly = readOnlyProxies.has( proxy ); | ||
prop.setValue( | ||
shouldProxy( value ) ? proxifyState( ns, value ) : value | ||
shouldProxy( value ) | ||
? proxifyState( ns, value, { readOnly } ) | ||
: value | ||
); | ||
@@ -153,2 +158,5 @@ } | ||
): boolean { | ||
if ( readOnlyProxies.has( receiver ) ) { | ||
return false; | ||
} | ||
setNamespace( getNamespaceFromProxy( receiver ) ); | ||
@@ -167,2 +175,6 @@ try { | ||
): boolean { | ||
if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { | ||
return false; | ||
} | ||
const isNew = ! ( key in target ); | ||
@@ -206,2 +218,6 @@ const result = Reflect.defineProperty( target, key, desc ); | ||
deleteProperty( target: object, key: string ): boolean { | ||
if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { | ||
return false; | ||
} | ||
const result = Reflect.deleteProperty( target, key ); | ||
@@ -238,4 +254,6 @@ | ||
* | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param namespace The namespace that will be associated to this proxy. | ||
* @param obj The object to proxify. | ||
* @param options Options. | ||
* @param options.readOnly Read-only. | ||
* | ||
@@ -249,4 +267,11 @@ * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to | ||
namespace: string, | ||
obj: T | ||
): T => createProxy( namespace, obj, stateHandlers ) as T; | ||
obj: T, | ||
options?: { readOnly?: boolean } | ||
): T => { | ||
const proxy = createProxy( namespace, obj, stateHandlers ) as T; | ||
if ( options?.readOnly ) { | ||
readOnlyProxies.add( proxy ); | ||
} | ||
return proxy; | ||
}; | ||
@@ -253,0 +278,0 @@ /** |
@@ -12,3 +12,3 @@ /* eslint-disable eslint-comments/disable-enable-pair */ | ||
*/ | ||
import { proxifyState, peek } from '../'; | ||
import { proxifyState, peek, deepMerge } from '../'; | ||
import { setScope, resetScope, getContext, getElement } from '../../scopes'; | ||
@@ -1269,3 +1269,200 @@ import { setNamespace, resetNamespace } from '../../namespaces'; | ||
} ); | ||
describe( 'read-only', () => { | ||
it( "should not allow modifying a prop's value", () => { | ||
const readOnlyState = proxifyState( | ||
'test', | ||
{ prop: 'value', nested: { prop: 'value' } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => { | ||
readOnlyState.prop = 'new value'; | ||
} ).toThrow(); | ||
expect( () => { | ||
readOnlyState.nested.prop = 'new value'; | ||
} ).toThrow(); | ||
} ); | ||
it( 'should not allow modifying a prop descriptor', () => { | ||
const readOnlyState = proxifyState( | ||
'test', | ||
{ prop: 'value', nested: { prop: 'value' } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => { | ||
Object.defineProperty( readOnlyState, 'prop', { | ||
get: () => 'value from getter', | ||
writable: true, | ||
enumerable: false, | ||
} ); | ||
} ).toThrow(); | ||
expect( () => { | ||
Object.defineProperty( readOnlyState.nested, 'prop', { | ||
get: () => 'value from getter', | ||
writable: true, | ||
enumerable: false, | ||
} ); | ||
} ).toThrow(); | ||
} ); | ||
it( 'should not allow adding new props', () => { | ||
const readOnlyState = proxifyState< any >( | ||
'test', | ||
{ prop: 'value', nested: { prop: 'value' } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => { | ||
readOnlyState.newProp = 'value'; | ||
} ).toThrow(); | ||
expect( () => { | ||
readOnlyState.nested.newProp = 'value'; | ||
} ).toThrow(); | ||
} ); | ||
it( 'should not allow removing props', () => { | ||
const readOnlyState = proxifyState< any >( | ||
'test', | ||
{ prop: 'value', nested: { prop: 'value' } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => { | ||
delete readOnlyState.prop; | ||
} ).toThrow(); | ||
expect( () => { | ||
delete readOnlyState.nested.prop; | ||
} ).toThrow(); | ||
} ); | ||
it( 'should not allow adding items to an array', () => { | ||
const readOnlyState = proxifyState( | ||
'test', | ||
{ array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => readOnlyState.array.push( 4 ) ).toThrow(); | ||
expect( () => readOnlyState.nested.array.push( 4 ) ).toThrow(); | ||
} ); | ||
it( 'should not allow removing items from an array', () => { | ||
const readOnlyState = proxifyState( | ||
'test', | ||
{ array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, | ||
{ readOnly: true } | ||
); | ||
expect( () => readOnlyState.array.pop() ).toThrow(); | ||
expect( () => readOnlyState.nested.array.pop() ).toThrow(); | ||
} ); | ||
it( 'should allow subscribing to prop changes', () => { | ||
const readOnlyState = proxifyState( | ||
'test', | ||
{ | ||
prop: 'value', | ||
nested: { prop: 'value' }, | ||
}, | ||
{ readOnly: true } | ||
); | ||
const spy1 = jest.fn( () => readOnlyState.prop ); | ||
const spy2 = jest.fn( () => readOnlyState.nested.prop ); | ||
effect( spy1 ); | ||
effect( spy2 ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 'value' ); | ||
expect( spy2 ).toHaveLastReturnedWith( 'value' ); | ||
deepMerge( readOnlyState, { prop: 'new value' } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 'new value' ); | ||
expect( spy2 ).toHaveLastReturnedWith( 'value' ); | ||
deepMerge( readOnlyState, { nested: { prop: 'new value' } } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 'new value' ); | ||
expect( spy2 ).toHaveLastReturnedWith( 'new value' ); | ||
} ); | ||
it( 'should allow subscribing to new props', () => { | ||
const readOnlyState = proxifyState< any >( | ||
'test', | ||
{ | ||
prop: 'value', | ||
nested: { prop: 'value' }, | ||
}, | ||
{ readOnly: true } | ||
); | ||
const spy1 = jest.fn( () => readOnlyState.newProp ); | ||
const spy2 = jest.fn( () => readOnlyState.nested.newProp ); | ||
effect( spy1 ); | ||
effect( spy2 ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( undefined ); | ||
expect( spy2 ).toHaveLastReturnedWith( undefined ); | ||
deepMerge( readOnlyState, { newProp: 'value' } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 'value' ); | ||
expect( spy2 ).toHaveLastReturnedWith( undefined ); | ||
deepMerge( readOnlyState, { nested: { newProp: 'value' } } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 'value' ); | ||
expect( spy2 ).toHaveLastReturnedWith( 'value' ); | ||
} ); | ||
it( 'should allow subscribing to array changes', () => { | ||
const readOnlyState = proxifyState< any >( | ||
'test', | ||
{ | ||
array: [ 1, 2, 3 ], | ||
nested: { array: [ 1, 2, 3 ] }, | ||
}, | ||
{ readOnly: true } | ||
); | ||
const spy1 = jest.fn( () => readOnlyState.array[ 0 ] ); | ||
const spy2 = jest.fn( () => readOnlyState.nested.array[ 0 ] ); | ||
effect( spy1 ); | ||
effect( spy2 ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 1 ); | ||
expect( spy2 ).toHaveLastReturnedWith( 1 ); | ||
deepMerge( readOnlyState, { array: [ 4, 5, 6 ] } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 1 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 4 ); | ||
expect( spy2 ).toHaveLastReturnedWith( 1 ); | ||
deepMerge( readOnlyState, { nested: { array: [] } } ); | ||
expect( spy1 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy2 ).toHaveBeenCalledTimes( 2 ); | ||
expect( spy1 ).toHaveLastReturnedWith( 4 ); | ||
expect( spy2 ).toHaveLastReturnedWith( undefined ); | ||
} ); | ||
} ); | ||
} ); | ||
} ); |
@@ -15,2 +15,3 @@ /** | ||
context: object; | ||
serverContext: object; | ||
ref: RefObject< HTMLElement >; | ||
@@ -100,1 +101,44 @@ attributes: createElement.JSX.HTMLAttributes; | ||
}; | ||
/** | ||
* Retrieves the part of the inherited context defined and updated from the | ||
* server. | ||
* | ||
* The object returned is read-only, and includes the context defined in PHP | ||
* with `wp_interactivity_data_wp_context()`, including the corresponding | ||
* inherited properties. When `actions.navigate()` is called, this object is | ||
* updated to reflect the changes in the new visited page, without affecting the | ||
* context returned by `getContext()`. Directives can subscribe to those changes | ||
* to update the context if needed. | ||
* | ||
* @example | ||
* ```js | ||
* store('...', { | ||
* callbacks: { | ||
* updateServerContext() { | ||
* const context = getContext(); | ||
* const serverContext = getServerContext(); | ||
* // Override some property with the new value that came from the server. | ||
* context.overridableProp = serverContext.overridableProp; | ||
* }, | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* @param namespace Store namespace. By default, the namespace where the calling | ||
* function exists is used. | ||
* @return The server context content. | ||
*/ | ||
export const getServerContext = < T extends object >( | ||
namespace?: string | ||
): T => { | ||
const scope = getScope(); | ||
if ( globalThis.SCRIPT_DEBUG ) { | ||
if ( ! scope ) { | ||
throw Error( | ||
'Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.' | ||
); | ||
} | ||
} | ||
return scope.serverContext[ namespace || getNamespace() ]; | ||
}; |
@@ -15,2 +15,3 @@ /** | ||
const storeConfigs = new Map(); | ||
const serverStates = new Map(); | ||
@@ -26,2 +27,35 @@ /** | ||
/** | ||
* Get 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 properites, 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?: string ) => { | ||
const ns = namespace || getNamespace(); | ||
if ( ! serverStates.has( ns ) ) { | ||
serverStates.set( ns, proxifyState( ns, {}, { readOnly: true } ) ); | ||
} | ||
return serverStates.get( ns ); | ||
}; | ||
interface StoreOptions { | ||
@@ -192,2 +226,3 @@ /** | ||
deepMerge( st.state, state, false ); | ||
deepMerge( getServerState( namespace ), state ); | ||
} ); | ||
@@ -194,0 +229,0 @@ } |
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
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
694855
9976