diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 4cfb87094ce109..99d13fbb83e8fb 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -2,7 +2,7 @@ * External dependencies */ import { parse as hpqParse } from 'hpq'; -import { mapValues, omit } from 'lodash'; +import { castArray, mapValues, omit } from 'lodash'; /** * WordPress dependencies @@ -155,13 +155,14 @@ export function getBlockAttributes( blockType, innerHTML, attributes ) { * Attempt to parse the innerHTML using using a supplied `deprecated` * definition. * - * @param {?Object} blockType Block type. - * @param {string} innerHTML Raw block content. - * @param {?Object} attributes Known block attributes (from delimiters). + * @param {?Object} blockType Block type. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Known block attributes (from delimiters). + * @param {?Array} innerBlocks Array of innerBlocks. * * @return {Object} Block attributes. */ -export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attributes ) { +export function getAttributesAndInnerBlocksFromDeprecatedVersion( blockType, innerHTML, attributes, innerBlocks ) { // Not all blocks need a deprecated definition so avoid unnecessary computational cycles // as early as possible when `deprecated` property is not supplied. if ( ! blockType.deprecated || ! blockType.deprecated.length ) { @@ -181,12 +182,22 @@ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attrib try { // Handle migration of older attributes into current version if necessary. const deprecatedBlockAttributes = getBlockAttributes( deprecatedBlockType, innerHTML, attributes ); - const migratedBlockAttributes = deprecatedBlockType.migrate ? deprecatedBlockType.migrate( deprecatedBlockAttributes ) : deprecatedBlockAttributes; // Attempt to validate the parsed block. Ignore if the the validation step fails. const isValid = isValidBlock( innerHTML, deprecatedBlockType, deprecatedBlockAttributes ); if ( isValid ) { - return migratedBlockAttributes; + const migratedBlockAttributesAndInnerBlocks = deprecatedBlockType.migrate && + deprecatedBlockType.migrate( deprecatedBlockAttributes, innerBlocks ); + + if ( migratedBlockAttributesAndInnerBlocks ) { + const [ + migratedAttributes, + migratedInnerBlocks = innerBlocks, + ] = castArray( migratedBlockAttributesAndInnerBlocks ); + return { attributes: migratedAttributes, innerBlocks: migratedInnerBlocks }; + } + + return { attributes: deprecatedBlockAttributes, innerBlocks }; } } catch ( error ) { // Ignore error, it means this deprecated version is invalid. @@ -275,13 +286,14 @@ export function createBlockWithFallback( blockNode ) { // This enables blocks to modify its attributes and markup structure without // invalidating content written in previous formats. if ( ! block.isValid ) { - const attributesParsedWithDeprecatedVersion = getAttributesFromDeprecatedVersion( - blockType, innerHTML, attributes + const attributesAndInnerBlocksParsedWithDeprecatedVersion = getAttributesAndInnerBlocksFromDeprecatedVersion( + blockType, innerHTML, attributes, block.innerBlocks ); - if ( attributesParsedWithDeprecatedVersion ) { + if ( attributesAndInnerBlocksParsedWithDeprecatedVersion ) { block.isValid = true; - block.attributes = attributesParsedWithDeprecatedVersion; + block.attributes = attributesAndInnerBlocksParsedWithDeprecatedVersion.attributes; + block.innerBlocks = attributesAndInnerBlocksParsedWithDeprecatedVersion.innerBlocks || []; } } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index ac304aa1a12a72..36208909138aa5 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -6,7 +6,7 @@ import { getBlockAttributes, asType, createBlockWithFallback, - getAttributesFromDeprecatedVersion, + getAttributesAndInnerBlocksFromDeprecatedVersion, default as parse, parseWithAttributeSchema, } from '../parser'; @@ -185,18 +185,18 @@ describe( 'block parser', () => { } ); } ); - describe( 'getAttributesFromDeprecatedVersion', () => { + describe( 'getAttributesAndInnerBlocksFromDeprecatedVersion', () => { it( 'should return undefined if the block has no deprecated versions', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( defaultBlockSettings, 'Bananas', {}, ); - expect( attributes ).toBeUndefined(); + expect( attributesAndInnerBlocks ).toBeUndefined(); } ); it( 'should return undefined if no valid deprecated version found', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( { name: 'core/test-block', ...defaultBlockSettings, @@ -211,13 +211,13 @@ describe( 'block parser', () => { 'Bananas', {}, ); - expect( attributes ).toBeUndefined(); + expect( attributesAndInnerBlocks ).toBeUndefined(); expect( console ).toHaveErrored(); expect( console ).toHaveWarned(); } ); it( 'should return the attributes parsed by the deprecated version', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( { name: 'core/test-block', ...defaultBlockSettings, @@ -238,7 +238,77 @@ describe( 'block parser', () => { 'Bananas', {}, ); - expect( attributes ).toEqual( { fruit: 'Bananas' } ); + expect( attributesAndInnerBlocks.attributes ).toEqual( { fruit: 'Bananas' } ); + } ); + + it( 'should return the innerBlocks if they are passed', () => { + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( + { + name: 'core/test-block', + ...defaultBlockSettings, + save: ( props ) =>
{ props.attributes.fruit }
, + deprecated: [ + { + attributes: { + fruit: { + type: 'string', + source: 'text', + selector: 'span', + }, + }, + save: ( props ) => { props.attributes.fruit }, + }, + ], + }, + 'Bananas', + {}, + [ { + name: 'core/test-block', + attributes: { aaa: 'bbb' }, + } ] + ); + + expect( attributesAndInnerBlocks.innerBlocks ).toHaveLength( 1 ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].name ).toEqual( 'core/test-block' ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].attributes ).toEqual( { aaa: 'bbb' } ); + } ); + + it( 'should be able to migrate attributes and innerBlocks', () => { + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( + { + name: 'core/test-block', + ...defaultBlockSettings, + save: ( props ) =>
{ props.attributes.fruit }
, + deprecated: [ + { + attributes: { + fruit: { + type: 'string', + source: 'text', + selector: 'span', + }, + }, + save: ( props ) => { props.attributes.fruit }, + migrate: ( attributes ) => { + return [ + { newFruit: attributes.fruit }, + [ { + name: 'core/test-block', + attributes: { aaa: 'bbb' }, + } ], + ]; + }, + }, + ], + }, + 'Bananas', + {}, + ); + + expect( attributesAndInnerBlocks.attributes ).toEqual( { newFruit: 'Bananas' } ); + expect( attributesAndInnerBlocks.innerBlocks ).toHaveLength( 1 ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].name ).toEqual( 'core/test-block' ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].attributes ).toEqual( { aaa: 'bbb' } ); } ); } ); diff --git a/docs/deprecated-blocks.md b/docs/deprecated-blocks.md index 345c398a734b56..3b952088e717df 100644 --- a/docs/deprecated-blocks.md +++ b/docs/deprecated-blocks.md @@ -173,3 +173,96 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { {% end %} In the example above we updated the markup of the block to use a `div` instead of `p` and rename the `text` attribute to `content`. + + +## Changing the innerBlocks + +Situations may exist where when migrating the block we may need to add or remove innerBlocks. +E.g: a block wants to migrate a title attribute to a paragraph innerBlock. + +### Example: + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate: function( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ].concat( innerBlocks ), + ]; + }, + + save: function( props ) { + return el( 'p', {}, props.attributes.title ); + }, + } + ] +} ); +``` +{% ESNext %} +```js +const { registerBlockType } = wp.blocks; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + save( props ) { + return

{ props.attributes.title }; + }, + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ...innerBlocks, + ], + ]; + }, + + save( props ) { + return

{ props.attributes.title }; + }, + } + ] +} ); +``` +{% end %} + +In the example above we updated the block to use an inner paragraph block with a title instead of a title attribute.