Skip to content

Commit

Permalink
Blocks: Add initial API for Block Patterns (#18270)
Browse files Browse the repository at this point in the history
* Blocks: Add initial API for Block Patterns

* Update packages/blocks/src/api/registration.js

* Convert all public APIs to experimental

* Expose block patterns registration APIs

* Fix typo in the name of selector

* Blocks: Update Patterns API integration with the store

* Update packages/blocks/src/store/selectors.js

Co-Authored-By: Andrew Duthie <andrew@andrewduthie.com>
  • Loading branch information
gziolo and aduth committed Nov 8, 2019
1 parent e1fb88e commit a71241c
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 17 deletions.
2 changes: 2 additions & 0 deletions packages/blocks/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export {
unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase
registerBlockStyle,
unregisterBlockStyle,
__experimentalRegisterBlockPattern,
__experimentalUnregisterBlockPattern,
} from './registration';
export {
isUnmodifiedDefaultBlock,
Expand Down
76 changes: 59 additions & 17 deletions packages/blocks/src/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ import { isValidIcon, normalizeIconObject } from './utils';
import { DEPRECATED_ENTRY_KEYS } from './constants';

/**
* Render behavior of a block type icon; one of a Dashicon slug, an element,
* An icon type definition. One of a Dashicon slug, an element,
* or a component.
*
* @typedef {(string|WPElement|WPComponent)} WPBlockTypeIconRender
* @typedef {(string|WPElement|WPComponent)} WPIcon
*
* @see https://developer.wordpress.org/resource/dashicons/
*/

/**
* Render behavior of a block type icon; one of a Dashicon slug, an element,
* or a component.
*
* @typedef {WPIcon} WPBlockTypeIconRender
*/

/**
* An object describing a normalized block type icon.
*
Expand All @@ -57,26 +64,41 @@ import { DEPRECATED_ENTRY_KEYS } from './constants';
* @typedef {(WPBlockTypeIconDescriptor|WPBlockTypeIconRender)} WPBlockTypeIcon
*/

/**
* An object describing a pattern defined for the block type.
*
* @typedef {Object} WPBlockPattern
*
* @property {string} name The unique and machine-readable name.
* @property {string} label A human-readable label.
* @property {WPIcon} [icon] An icon helping to visualize the pattern.
* @property {boolean} [isDefault] Indicates whether the current pattern is the default one.
* Defaults to `false`.
* @property {Object} [attributes] Values which override block attributes.
* @property {Array[]} [innerBlocks] Initial configuration of nested blocks.
*/

/**
* Defined behavior of a block type.
*
* @typedef {Object} WPBlock
*
* @property {string} name Block type's namespaced name.
* @property {string} title Human-readable block type label.
* @property {string} category Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPBlockTypeIcon} [icon] Block type icon.
* @property {string[]} [keywords] Additional keywords to produce block
* type as result in search interfaces.
* @property {Object} [attributes] Block type attributes.
* @property {WPComponent} [save] Optional component describing
* serialized markup structure of a
* block type.
* @property {WPComponent} edit Component rendering an element to
* manipulate the attributes of a block
* in the context of an editor.
* @property {string} name Block type's namespaced name.
* @property {string} title Human-readable block type label.
* @property {string} category Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPBlockTypeIcon} [icon] Block type icon.
* @property {string[]} [keywords] Additional keywords to produce block
* type as result in search interfaces.
* @property {Object} [attributes] Block type attributes.
* @property {WPComponent} [save] Optional component describing
* serialized markup structure of a
* block type.
* @property {WPComponent} edit Component rendering an element to
* manipulate the attributes of a block
* in the context of an editor.
* @property {WPBlockPattern[]} [patterns] The list of block patterns.
*/

/**
Expand Down Expand Up @@ -436,3 +458,23 @@ export const registerBlockStyle = ( blockName, styleVariation ) => {
export const unregisterBlockStyle = ( blockName, styleVariationName ) => {
dispatch( 'core/blocks' ).removeBlockStyles( blockName, styleVariationName );
};

/**
* Registers a new block pattern for the given block.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {WPBlockPattern} pattern Object describing a block pattern.
*/
export const __experimentalRegisterBlockPattern = ( blockName, pattern ) => {
dispatch( 'core/blocks' ).__experimentalAddBlockPatterns( blockName, pattern );
};

/**
* Unregisters a block pattern defined for the given block.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {string} patternName Name of the pattern defined for the block.
*/
export const __experimentalUnregisterBlockPattern = ( blockName, patternName ) => {
dispatch( 'core/blocks' ).__experimentalRemoveBlockPatterns( blockName, patternName );
};
32 changes: 32 additions & 0 deletions packages/blocks/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,38 @@ export function removeBlockStyles( blockName, styleNames ) {
};
}

/**
* Returns an action object used in signalling that new block patterns have been added.
*
* @param {string} blockName Block name.
* @param {WPBlockPattern|WPBlockPattern[]} patterns Block patterns.
*
* @return {Object} Action object.
*/
export function __experimentalAddBlockPatterns( blockName, patterns ) {
return {
type: 'ADD_BLOCK_PATTERNS',
patterns: castArray( patterns ),
blockName,
};
}

/**
* Returns an action object used in signalling that block patterns have been removed.
*
* @param {string} blockName Block name.
* @param {string|string[]} patternNames Block pattern names.
*
* @return {Object} Action object.
*/
export function __experimentalRemoveBlockPatterns( blockName, patternNames ) {
return {
type: 'REMOVE_BLOCK_PATTERNS',
patternNames: castArray( patternNames ),
blockName,
};
}

/**
* Returns an action object used to set the default block name.
*
Expand Down
42 changes: 42 additions & 0 deletions packages/blocks/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ export function blockStyles( state = {}, action ) {
return state;
}

/**
* Reducer managing the block patterns.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export function blockPatterns( state = {}, action ) {
switch ( action.type ) {
case 'ADD_BLOCK_TYPES':
return {
...state,
...mapValues( keyBy( action.blockTypes, 'name' ), ( blockType ) => {
return uniqBy( [
...get( blockType, [ 'patterns' ], [] ),
...get( state, [ blockType.name ], [] ),
], ( style ) => style.name );
} ),
};
case 'ADD_BLOCK_PATTERNS':
return {
...state,
[ action.blockName ]: uniqBy( [
...get( state, [ action.blockName ], [] ),
...( action.patterns ),
], ( pattern ) => pattern.name ),
};
case 'REMOVE_BLOCK_PATTERNS':
return {
...state,
[ action.blockName ]: filter(
get( state, [ action.blockName ], [] ),
( pattern ) => action.patternNames.indexOf( pattern.name ) === -1,
),
};
}

return state;
}

/**
* Higher-order Reducer creating a reducer keeping track of given block name.
*
Expand Down Expand Up @@ -162,6 +203,7 @@ export function categories( state = DEFAULT_CATEGORIES, action ) {
export default combineReducers( {
blockTypes,
blockStyles,
blockPatterns,
defaultBlockName,
freeformFallbackBlockName,
unregisteredFallbackBlockName,
Expand Down
12 changes: 12 additions & 0 deletions packages/blocks/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ export function getBlockStyles( state, name ) {
return state.blockStyles[ name ];
}

/**
* Returns block patterns by block name.
*
* @param {Object} state Data state.
* @param {string} blockName Block type name.
*
* @return {(WPBlockPattern[]|void)} Block patterns.
*/
export function __experimentalGetBlockPatterns( state, blockName ) {
return state.blockPatterns[ blockName ];
}

/**
* Returns all the available categories.
*
Expand Down
43 changes: 43 additions & 0 deletions packages/blocks/src/store/test/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Internal dependencies
*/
import {
__experimentalAddBlockPatterns,
__experimentalRemoveBlockPatterns,
} from '../actions';

describe( 'actions', () => {
describe( 'addBlockPatterns', () => {
const blockName = 'block/name';
const patternName = 'my-pattern';

it( 'should return the ADD_BLOCK_PATTERNS action', () => {
const pattern = {
name: patternName,
label: 'My Pattern',
attributes: {
example: 'foo',
},
};
const result = __experimentalAddBlockPatterns( blockName, pattern );
expect( result ).toEqual( {
type: 'ADD_BLOCK_PATTERNS',
patterns: [
pattern,
],
blockName,
} );
} );

it( 'should return the REMOVE_BLOCK_PATTERNS action', () => {
const result = __experimentalRemoveBlockPatterns( blockName, patternName );
expect( result ).toEqual( {
type: 'REMOVE_BLOCK_PATTERNS',
patternNames: [
patternName,
],
blockName,
} );
} );
} );
} );
108 changes: 108 additions & 0 deletions packages/blocks/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import deepFreeze from 'deep-freeze';
* Internal dependencies
*/
import {
__experimentalAddBlockPatterns,
addBlockTypes,
__experimentalRemoveBlockPatterns,
} from '../actions';
import {
blockPatterns,
blockStyles,
blockTypes,
categories,
Expand Down Expand Up @@ -136,6 +142,108 @@ describe( 'blockStyles', () => {
} );
} );

describe( 'blockPatterns', () => {
const blockName = 'block/name';

const blockPatternName = 'pattern-name';
const blockPattern = {
name: blockPatternName,
label: 'My pattern',
};

const secondBlockPatternName = 'second-pattern-name';
const secondBlockPattern = {
name: secondBlockPatternName,
label: 'My Second Pattern',
};

it( 'should return an empty object as default state', () => {
const state = blockPatterns( undefined, {} );

expect( state ).toEqual( {} );
} );

it( 'should add a new block pattern when no pattern register', () => {
const initialState = deepFreeze( {} );

const state = blockPatterns(
initialState,
__experimentalAddBlockPatterns( blockName, blockPattern )
);

expect( state ).toEqual( {
[ blockName ]: [
blockPattern,
],
} );
} );

it( 'should add another pattern when a block pattern already present for the block', () => {
const initialState = deepFreeze( {
[ blockName ]: [
blockPattern,
],
} );

const state = blockPatterns(
initialState,
__experimentalAddBlockPatterns( blockName, secondBlockPattern ),
);

expect( state ).toEqual( {
[ blockName ]: [
blockPattern,
secondBlockPattern,
],
} );
} );

it( 'should prepend block patterns added when adding a block', () => {
const initialState = deepFreeze( {
[ blockName ]: [
secondBlockPattern,
],
} );

const state = blockPatterns(
initialState,
addBlockTypes( {
name: blockName,
patterns: [
blockPattern,
],
} )
);

expect( state ).toEqual( {
[ blockName ]: [
blockPattern,
secondBlockPattern,
],
} );
} );

it( 'should remove a block pattern', () => {
const initialState = deepFreeze( {
[ blockName ]: [
blockPattern,
secondBlockPattern,
],
} );

const state = blockPatterns(
initialState,
__experimentalRemoveBlockPatterns( blockName, blockPatternName )
);

expect( state ).toEqual( {
[ blockName ]: [
secondBlockPattern,
],
} );
} );
} );

describe( 'defaultBlockName', () => {
it( 'should return null as default state', () => {
expect( defaultBlockName( undefined, {} ) ).toBeNull();
Expand Down
Loading

0 comments on commit a71241c

Please sign in to comment.