diff --git a/blocks/api/index.js b/blocks/api/index.js
index 828e6cce27b31..cc7d47cd0ac11 100644
--- a/blocks/api/index.js
+++ b/blocks/api/index.js
@@ -33,3 +33,7 @@ export {
export {
isUnmodifiedDefaultBlock,
} from './utils';
+export {
+ doBlocksMatchTemplate,
+ synchronizeBlocksWithTemplate,
+} from './templates';
diff --git a/blocks/api/templates.js b/blocks/api/templates.js
new file mode 100644
index 0000000000000..b0cc2daf1aa96
--- /dev/null
+++ b/blocks/api/templates.js
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import { every, map } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { createBlock } from './factory';
+
+/**
+ * Checks whether a list of blocks matches a template by comparing the block names.
+ *
+ * @param {Array} blocks Block list.
+ * @param {Array} template Block template.
+ *
+ * @return {boolean} Whether the list of blocks matches a templates
+ */
+export function doBlocksMatchTemplate( blocks = [], template = [] ) {
+ return (
+ blocks.length === template.length &&
+ every( template, ( [ name, , innerBlocksTemplate ], index ) => {
+ const block = blocks[ index ];
+ return (
+ name === block.name &&
+ doBlocksMatchTemplate( block.innerBlocks, innerBlocksTemplate )
+ );
+ } )
+ );
+}
+
+/**
+ * Synchronize a block list with a block template.
+ *
+ * Synchronnizing a block list with a block template means that we loop over the blocks
+ * keep the block as is if it matches the block at the same position in the template
+ * (If it has the same name) and if doesn't match, we create a new block based on the template.
+ * Extra blocks not present in the template are removed.
+ *
+ * @param {Array} blocks Block list.
+ * @param {Array} template Block template.
+ *
+ * @return {Array} Updated Block list.
+ */
+export function synchronizeBlocksWithTemplate( blocks = [], template = [] ) {
+ return map( template, ( [ name, attributes, innerBlocksTemplate ], index ) => {
+ const block = blocks[ index ];
+
+ if ( block && block.name === name ) {
+ const innerBlocks = synchronizeBlocksWithTemplate( block.innerBlocks, innerBlocksTemplate );
+ return { ...block, innerBlocks };
+ }
+
+ return createBlock(
+ name,
+ attributes,
+ synchronizeBlocksWithTemplate( [], innerBlocksTemplate )
+ );
+ } );
+}
diff --git a/blocks/api/test/templates.js b/blocks/api/test/templates.js
new file mode 100644
index 0000000000000..1ee52fdadb756
--- /dev/null
+++ b/blocks/api/test/templates.js
@@ -0,0 +1,177 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { createBlock } from '../factory';
+import { getBlockTypes, unregisterBlockType, registerBlockType } from '../registration';
+import { doBlocksMatchTemplate, synchronizeBlocksWithTemplate } from '../templates';
+
+describe( 'templates', () => {
+ afterEach( () => {
+ getBlockTypes().forEach( ( block ) => {
+ unregisterBlockType( block.name );
+ } );
+ } );
+
+ beforeEach( () => {
+ registerBlockType( 'core/test-block', {
+ attributes: {},
+ save: noop,
+ category: 'common',
+ title: 'test block',
+ } );
+
+ registerBlockType( 'core/test-block-2', {
+ attributes: {},
+ save: noop,
+ category: 'common',
+ title: 'test block',
+ } );
+ } );
+
+ describe( 'doBlocksMatchTemplate', () => {
+ it( 'return true if for empty templates and blocks', () => {
+ expect( doBlocksMatchTemplate() ).toBe( true );
+ } );
+
+ it( 'return true if the template matches the blocks', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2' ],
+ [ 'core/test-block-2' ],
+ ];
+ const blockList = [
+ createBlock( 'core/test-block' ),
+ createBlock( 'core/test-block-2' ),
+ createBlock( 'core/test-block-2' ),
+ ];
+ expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true );
+ } );
+
+ it( 'return true if the template matches the blocks with nested blocks', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2', {}, [
+ [ 'core/test-block' ],
+ ] ],
+ [ 'core/test-block-2' ],
+ ];
+ const blockList = [
+ createBlock( 'core/test-block' ),
+ createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block' ) ] ),
+ createBlock( 'core/test-block-2' ),
+ ];
+ expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true );
+ } );
+
+ it( 'return false if the template length doesn\'t match the blocks length', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2' ],
+ ];
+ const blockList = [
+ createBlock( 'core/test-block' ),
+ createBlock( 'core/test-block-2' ),
+ createBlock( 'core/test-block-2' ),
+ ];
+ expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false );
+ } );
+
+ it( 'return false if the nested template doesn\'t match the blocks', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2', {}, [
+ [ 'core/test-block' ],
+ ] ],
+ [ 'core/test-block-2' ],
+ ];
+ const blockList = [
+ createBlock( 'core/test-block' ),
+ createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block-2' ) ] ),
+ createBlock( 'core/test-block-2' ),
+ ];
+ expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false );
+ } );
+ } );
+
+ describe( 'synchronizeBlocksWithTemplate', () => {
+ it( 'should create blocks for each template entry', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2' ],
+ [ 'core/test-block-2' ],
+ ];
+ const blockList = [];
+ expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
+ { name: 'core/test-block' },
+ { name: 'core/test-block-2' },
+ { name: 'core/test-block-2' },
+ ] );
+ } );
+
+ it( 'should create nested blocks', () => {
+ const template = [
+ [ 'core/test-block', {}, [
+ [ 'core/test-block-2' ],
+ ] ],
+ ];
+ const blockList = [];
+ expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
+ { name: 'core/test-block', innerBlocks: [
+ { name: 'core/test-block-2' },
+ ] },
+ ] );
+ } );
+
+ it( 'should append blocks if more blocks in the template', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2' ],
+ [ 'core/test-block-2' ],
+ ];
+
+ const block1 = createBlock( 'core/test-block' );
+ const block2 = createBlock( 'core/test-block-2' );
+ const blockList = [ block1, block2 ];
+ expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
+ block1,
+ block2,
+ { name: 'core/test-block-2' },
+ ] );
+ } );
+
+ it( 'should replace blocks if not matching blocks are found', () => {
+ const template = [
+ [ 'core/test-block' ],
+ [ 'core/test-block-2' ],
+ [ 'core/test-block-2' ],
+ ];
+
+ const block1 = createBlock( 'core/test-block' );
+ const block2 = createBlock( 'core/test-block' );
+ const blockList = [ block1, block2 ];
+ expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
+ block1,
+ { name: 'core/test-block-2' },
+ { name: 'core/test-block-2' },
+ ] );
+ } );
+
+ it( 'should remove blocks if extra blocks are found', () => {
+ const template = [
+ [ 'core/test-block' ],
+ ];
+
+ const block1 = createBlock( 'core/test-block' );
+ const block2 = createBlock( 'core/test-block' );
+ const blockList = [ block1, block2 ];
+ expect( synchronizeBlocksWithTemplate( blockList, template ) ).toEqual( [
+ block1,
+ ] );
+ } );
+ } );
+} );
diff --git a/components/notice/index.js b/components/notice/index.js
index c2d1a8273400b..e8fa2ca7482a1 100644
--- a/components/notice/index.js
+++ b/components/notice/index.js
@@ -14,13 +14,13 @@ import { __ } from '@wordpress/i18n';
*/
import './style.scss';
-function Notice( { status, content, onRemove = noop, isDismissible = true } ) {
- const className = classnames( 'notice notice-alt notice-' + status, {
+function Notice( { className, status, children, onRemove = noop, isDismissible = true } ) {
+ const classNames = classnames( className, 'notice notice-alt notice-' + status, {
'is-dismissible': isDismissible,
} );
return (
-
- { isString( content ) ?
{ content }
: content }
+
+ { isString( children ) ?
{ children }
: children }
{ isDismissible && (