diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 8c262ebeee8d19..2f2094bed7f4d4 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -976,6 +976,47 @@ The following set of react hooks available to import from the `@wordpress/core-d +### useEntityBlockEditor + +Hook that returns block content getters and setters for the nearest provided entity of the specified type. + +The return value has the shape `[ blocks, onInput, onChange ]`. `onInput` is for block changes that don't create undo levels or dirty the post, non-persistent changes, and `onChange` is for persistent changes. They map directly to the props of a `BlockEditorProvider` and are intended to be used with it, or similar components or hooks. + +_Parameters_ + +- _kind_ `string`: The entity kind. +- _name_ `string`: The entity name. +- _options_ `Object`: +- _options.id_ `[string]`: An entity ID to use instead of the context-provided one. + +_Returns_ + +- `[unknown[], Function, Function]`: The block array and setters. + +### useEntityId + +Hook that returns the ID for the nearest provided entity of the specified type. + +_Parameters_ + +- _kind_ `string`: The entity kind. +- _name_ `string`: The entity name. + +### useEntityProp + +Hook that returns the value and a setter for the specified property of the nearest provided entity of the specified type. + +_Parameters_ + +- _kind_ `string`: The entity kind. +- _name_ `string`: The entity name. +- _prop_ `string`: The property name. +- _\_id_ `[string]`: An entity ID to use instead of the context-provided one. + +_Returns_ + +- `[*, Function, *]`: An array where the first item is the property value, the second is the setter and the third is the full value object from REST API containing more information like `raw`, `rendered` and `protected` props. + ### useEntityRecord Resolves the specified entity record. diff --git a/packages/core-data/src/entity-context.js b/packages/core-data/src/entity-context.js new file mode 100644 index 00000000000000..9a2e597527065a --- /dev/null +++ b/packages/core-data/src/entity-context.js @@ -0,0 +1,6 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +export const EntityContext = createContext( {} ); diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index bc1fe29759950b..322d53f7f3a29d 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -1,24 +1,12 @@ /** * WordPress dependencies */ -import { - createContext, - useContext, - useCallback, - useMemo, -} from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { STORE_NAME } from './name'; -import { updateFootnotesFromMeta } from './footnotes'; - -const EMPTY_ARRAY = []; - -const EntityContext = createContext( {} ); +import { EntityContext } from './entity-context'; /** * Context provider component for providing @@ -51,198 +39,3 @@ export default function EntityProvider( { kind, type: name, id, children } ) { ); } - -/** - * Hook that returns the ID for the nearest - * provided entity of the specified type. - * - * @param {string} kind The entity kind. - * @param {string} name The entity name. - */ -export function useEntityId( kind, name ) { - const context = useContext( EntityContext ); - return context?.[ kind ]?.[ name ]; -} - -/** - * Hook that returns the value and a setter for the - * specified property of the nearest provided - * entity of the specified type. - * - * @param {string} kind The entity kind. - * @param {string} name The entity name. - * @param {string} prop The property name. - * @param {string} [_id] An entity ID to use instead of the context-provided one. - * - * @return {[*, Function, *]} An array where the first item is the - * property value, the second is the - * setter and the third is the full value - * object from REST API containing more - * information like `raw`, `rendered` and - * `protected` props. - */ -export function useEntityProp( kind, name, prop, _id ) { - const providerId = useEntityId( kind, name ); - const id = _id ?? providerId; - - const { value, fullValue } = useSelect( - ( select ) => { - const { getEntityRecord, getEditedEntityRecord } = - select( STORE_NAME ); - const record = getEntityRecord( kind, name, id ); // Trigger resolver. - const editedRecord = getEditedEntityRecord( kind, name, id ); - return record && editedRecord - ? { - value: editedRecord[ prop ], - fullValue: record[ prop ], - } - : {}; - }, - [ kind, name, id, prop ] - ); - const { editEntityRecord } = useDispatch( STORE_NAME ); - const setValue = useCallback( - ( newValue ) => { - editEntityRecord( kind, name, id, { - [ prop ]: newValue, - } ); - }, - [ editEntityRecord, kind, name, id, prop ] - ); - - return [ value, setValue, fullValue ]; -} - -const parsedBlocksCache = new WeakMap(); - -/** - * Hook that returns block content getters and setters for - * the nearest provided entity of the specified type. - * - * The return value has the shape `[ blocks, onInput, onChange ]`. - * `onInput` is for block changes that don't create undo levels - * or dirty the post, non-persistent changes, and `onChange` is for - * persistent changes. They map directly to the props of a - * `BlockEditorProvider` and are intended to be used with it, - * or similar components or hooks. - * - * @param {string} kind The entity kind. - * @param {string} name The entity name. - * @param {Object} options - * @param {string} [options.id] An entity ID to use instead of the context-provided one. - * - * @return {[unknown[], Function, Function]} The block array and setters. - */ -export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { - const providerId = useEntityId( kind, name ); - const id = _id ?? providerId; - const { getEntityRecord, getEntityRecordEdits } = useSelect( STORE_NAME ); - const { content, editedBlocks, meta } = useSelect( - ( select ) => { - if ( ! id ) { - return {}; - } - const { getEditedEntityRecord } = select( STORE_NAME ); - const editedRecord = getEditedEntityRecord( kind, name, id ); - return { - editedBlocks: editedRecord.blocks, - content: editedRecord.content, - meta: editedRecord.meta, - }; - }, - [ kind, name, id ] - ); - const { __unstableCreateUndoLevel, editEntityRecord } = - useDispatch( STORE_NAME ); - - const blocks = useMemo( () => { - if ( ! id ) { - return undefined; - } - - if ( editedBlocks ) { - return editedBlocks; - } - - if ( ! content || typeof content !== 'string' ) { - return EMPTY_ARRAY; - } - - // If there's an edit, cache the parsed blocks by the edit. - // If not, cache by the original enity record. - const edits = getEntityRecordEdits( kind, name, id ); - const isUnedited = ! edits || ! Object.keys( edits ).length; - const cackeKey = isUnedited ? getEntityRecord( kind, name, id ) : edits; - let _blocks = parsedBlocksCache.get( cackeKey ); - - if ( ! _blocks ) { - _blocks = parse( content ); - parsedBlocksCache.set( cackeKey, _blocks ); - } - - return _blocks; - }, [ - kind, - name, - id, - editedBlocks, - content, - getEntityRecord, - getEntityRecordEdits, - ] ); - - const updateFootnotes = useCallback( - ( _blocks ) => updateFootnotesFromMeta( _blocks, meta ), - [ meta ] - ); - - const onChange = useCallback( - ( newBlocks, options ) => { - const noChange = blocks === newBlocks; - if ( noChange ) { - return __unstableCreateUndoLevel( kind, name, id ); - } - const { selection, ...rest } = options; - - // We create a new function here on every persistent edit - // to make sure the edit makes the post dirty and creates - // a new undo level. - const edits = { - selection, - content: ( { blocks: blocksForSerialization = [] } ) => - __unstableSerializeAndClean( blocksForSerialization ), - ...updateFootnotes( newBlocks ), - }; - - editEntityRecord( kind, name, id, edits, { - isCached: false, - ...rest, - } ); - }, - [ - kind, - name, - id, - blocks, - updateFootnotes, - __unstableCreateUndoLevel, - editEntityRecord, - ] - ); - - const onInput = useCallback( - ( newBlocks, options ) => { - const { selection, ...rest } = options; - const footnotesChanges = updateFootnotes( newBlocks ); - const edits = { selection, ...footnotesChanges }; - - editEntityRecord( kind, name, id, edits, { - isCached: true, - ...rest, - } ); - }, - [ kind, name, id, updateFootnotes, editEntityRecord ] - ); - - return [ blocks, onInput, onChange ]; -} diff --git a/packages/core-data/src/hooks/index.ts b/packages/core-data/src/hooks/index.ts index a12269616b2f0c..8e5210acdd448c 100644 --- a/packages/core-data/src/hooks/index.ts +++ b/packages/core-data/src/hooks/index.ts @@ -10,3 +10,6 @@ export { default as useResourcePermissions, __experimentalUseResourcePermissions, } from './use-resource-permissions'; +export { default as useEntityBlockEditor } from './use-entity-block-editor'; +export { default as useEntityId } from './use-entity-id'; +export { default as useEntityProp } from './use-entity-prop'; diff --git a/packages/core-data/src/hooks/use-entity-block-editor.js b/packages/core-data/src/hooks/use-entity-block-editor.js new file mode 100644 index 00000000000000..56238097618e1f --- /dev/null +++ b/packages/core-data/src/hooks/use-entity-block-editor.js @@ -0,0 +1,148 @@ +/** + * WordPress dependencies + */ +import { useCallback, useMemo } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../name'; +import useEntityId from './use-entity-id'; +import { updateFootnotesFromMeta } from '../footnotes'; + +const EMPTY_ARRAY = []; +const parsedBlocksCache = new WeakMap(); + +/** + * Hook that returns block content getters and setters for + * the nearest provided entity of the specified type. + * + * The return value has the shape `[ blocks, onInput, onChange ]`. + * `onInput` is for block changes that don't create undo levels + * or dirty the post, non-persistent changes, and `onChange` is for + * persistent changes. They map directly to the props of a + * `BlockEditorProvider` and are intended to be used with it, + * or similar components or hooks. + * + * @param {string} kind The entity kind. + * @param {string} name The entity name. + * @param {Object} options + * @param {string} [options.id] An entity ID to use instead of the context-provided one. + * + * @return {[unknown[], Function, Function]} The block array and setters. + */ +export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) { + const providerId = useEntityId( kind, name ); + const id = _id ?? providerId; + const { getEntityRecord, getEntityRecordEdits } = useSelect( STORE_NAME ); + const { content, editedBlocks, meta } = useSelect( + ( select ) => { + if ( ! id ) { + return {}; + } + const { getEditedEntityRecord } = select( STORE_NAME ); + const editedRecord = getEditedEntityRecord( kind, name, id ); + return { + editedBlocks: editedRecord.blocks, + content: editedRecord.content, + meta: editedRecord.meta, + }; + }, + [ kind, name, id ] + ); + const { __unstableCreateUndoLevel, editEntityRecord } = + useDispatch( STORE_NAME ); + + const blocks = useMemo( () => { + if ( ! id ) { + return undefined; + } + + if ( editedBlocks ) { + return editedBlocks; + } + + if ( ! content || typeof content !== 'string' ) { + return EMPTY_ARRAY; + } + + // If there's an edit, cache the parsed blocks by the edit. + // If not, cache by the original enity record. + const edits = getEntityRecordEdits( kind, name, id ); + const isUnedited = ! edits || ! Object.keys( edits ).length; + const cackeKey = isUnedited ? getEntityRecord( kind, name, id ) : edits; + let _blocks = parsedBlocksCache.get( cackeKey ); + + if ( ! _blocks ) { + _blocks = parse( content ); + parsedBlocksCache.set( cackeKey, _blocks ); + } + + return _blocks; + }, [ + kind, + name, + id, + editedBlocks, + content, + getEntityRecord, + getEntityRecordEdits, + ] ); + + const updateFootnotes = useCallback( + ( _blocks ) => updateFootnotesFromMeta( _blocks, meta ), + [ meta ] + ); + + const onChange = useCallback( + ( newBlocks, options ) => { + const noChange = blocks === newBlocks; + if ( noChange ) { + return __unstableCreateUndoLevel( kind, name, id ); + } + const { selection, ...rest } = options; + + // We create a new function here on every persistent edit + // to make sure the edit makes the post dirty and creates + // a new undo level. + const edits = { + selection, + content: ( { blocks: blocksForSerialization = [] } ) => + __unstableSerializeAndClean( blocksForSerialization ), + ...updateFootnotes( newBlocks ), + }; + + editEntityRecord( kind, name, id, edits, { + isCached: false, + ...rest, + } ); + }, + [ + kind, + name, + id, + blocks, + updateFootnotes, + __unstableCreateUndoLevel, + editEntityRecord, + ] + ); + + const onInput = useCallback( + ( newBlocks, options ) => { + const { selection, ...rest } = options; + const footnotesChanges = updateFootnotes( newBlocks ); + const edits = { selection, ...footnotesChanges }; + + editEntityRecord( kind, name, id, edits, { + isCached: true, + ...rest, + } ); + }, + [ kind, name, id, updateFootnotes, editEntityRecord ] + ); + + return [ blocks, onInput, onChange ]; +} diff --git a/packages/core-data/src/hooks/use-entity-id.js b/packages/core-data/src/hooks/use-entity-id.js new file mode 100644 index 00000000000000..7fb9d33fb7bdf3 --- /dev/null +++ b/packages/core-data/src/hooks/use-entity-id.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { EntityContext } from '../entity-context'; + +/** + * Hook that returns the ID for the nearest + * provided entity of the specified type. + * + * @param {string} kind The entity kind. + * @param {string} name The entity name. + */ +export default function useEntityId( kind, name ) { + const context = useContext( EntityContext ); + return context?.[ kind ]?.[ name ]; +} diff --git a/packages/core-data/src/hooks/use-entity-prop.js b/packages/core-data/src/hooks/use-entity-prop.js new file mode 100644 index 00000000000000..2894721410dfdf --- /dev/null +++ b/packages/core-data/src/hooks/use-entity-prop.js @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../name'; +import useEntityId from './use-entity-id'; + +/** + * Hook that returns the value and a setter for the + * specified property of the nearest provided + * entity of the specified type. + * + * @param {string} kind The entity kind. + * @param {string} name The entity name. + * @param {string} prop The property name. + * @param {string} [_id] An entity ID to use instead of the context-provided one. + * + * @return {[*, Function, *]} An array where the first item is the + * property value, the second is the + * setter and the third is the full value + * object from REST API containing more + * information like `raw`, `rendered` and + * `protected` props. + */ +export default function useEntityProp( kind, name, prop, _id ) { + const providerId = useEntityId( kind, name ); + const id = _id ?? providerId; + + const { value, fullValue } = useSelect( + ( select ) => { + const { getEntityRecord, getEditedEntityRecord } = + select( STORE_NAME ); + const record = getEntityRecord( kind, name, id ); // Trigger resolver. + const editedRecord = getEditedEntityRecord( kind, name, id ); + return record && editedRecord + ? { + value: editedRecord[ prop ], + fullValue: record[ prop ], + } + : {}; + }, + [ kind, name, id, prop ] + ); + const { editEntityRecord } = useDispatch( STORE_NAME ); + const setValue = useCallback( + ( newValue ) => { + editEntityRecord( kind, name, id, { + [ prop ]: newValue, + } ); + }, + [ editEntityRecord, kind, name, id, prop ] + ); + + return [ value, setValue, fullValue ]; +} diff --git a/packages/core-data/src/test/entity-provider.js b/packages/core-data/src/test/entity-provider.js index 72bb083153e06a..6b0b7bd5ef77a8 100644 --- a/packages/core-data/src/test/entity-provider.js +++ b/packages/core-data/src/test/entity-provider.js @@ -21,7 +21,7 @@ import { unregisterFormatType } from '@wordpress/rich-text'; * Internal dependencies */ import { store as coreDataStore } from '../index'; -import { useEntityBlockEditor } from '../entity-provider'; +import useEntityBlockEditor from '../hooks/use-entity-block-editor'; const postTypeConfig = { kind: 'postType',