Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InnerBlocks: introduce prop to specify render callback for each block. #24232

Merged
merged 13 commits into from
Aug 15, 2020

Conversation

ZebulanStanphill
Copy link
Member

@ZebulanStanphill ZebulanStanphill commented Jul 28, 2020

Description

This PR adds an __experimentalItemWrapper prop to InnerBlocks and InnerBlocks.Content, allowing blocks to specify a component to wrap each block. The feature is designed to help solve issues discussed in #23915 and #24039.

Update 2020-08-05: this PR has been revised to use a more flexible callback-based API. This allows you to not only wrap all child blocks with multiple levels of parent elements, but also append stuff before and after each of them.

<InnerBlocks
	__experimentalItemCallback={ ( item ) => (
		<>
			<section>{ item }</section>
			<p>You can do cool/weird stuff like this now.</p>
		</>
	) }
/>

To pull this off, it also ends up modifying BlockList, BlockListBlock, BlockContentProvider, and serializeBlock. The changes should have no effect on anything that doesn't use the new feature.

Reasoning behind my decision to take this approach.

How has this been tested?

It has not yet been tested. Currently planning on testing this via #24039.

Todo

  • Get some technical feedback to make sure the implementation is sane.
  • Add React Native edit implementation.
  • Make sure the React Native implementation actually works.
  • Add tests.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@ZebulanStanphill ZebulanStanphill added [Feature] Nested / Inner Blocks Anything related to the experience of nested/inner blocks inside a larger container, like Group or P Needs Technical Feedback Needs testing from a developer perspective. [Type] Experimental Experimental feature or API. labels Jul 28, 2020
@github-actions
Copy link

github-actions bot commented Jul 28, 2020

Size Change: +174 B (0%)

Total Size: 1.16 MB

Filename Size Change
build/block-editor/index.js 126 kB +112 B (0%)
build/blocks/index.js 48.4 kB +62 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.67 kB 0 B
build/api-fetch/index.js 3.44 kB 0 B
build/autop/index.js 2.82 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 7.96 kB 0 B
build/block-directory/style-rtl.css 953 B 0 B
build/block-directory/style.css 952 B 0 B
build/block-editor/style-rtl.css 10.6 kB 0 B
build/block-editor/style.css 10.6 kB 0 B
build/block-library/editor-rtl.css 8.36 kB 0 B
build/block-library/editor.css 8.36 kB 0 B
build/block-library/index.js 132 kB 0 B
build/block-library/style-rtl.css 7.49 kB 0 B
build/block-library/style.css 7.49 kB 0 B
build/block-library/theme-rtl.css 729 B 0 B
build/block-library/theme.css 730 B 0 B
build/block-serialization-default-parser/index.js 1.88 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/components/index.js 200 kB 0 B
build/components/style-rtl.css 15.7 kB 0 B
build/components/style.css 15.7 kB 0 B
build/compose/index.js 9.68 kB 0 B
build/core-data/index.js 11.8 kB 0 B
build/data-controls/index.js 1.29 kB 0 B
build/data/index.js 8.56 kB 0 B
build/date/index.js 5.38 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 3.23 kB 0 B
build/edit-navigation/index.js 10.9 kB 0 B
build/edit-navigation/style-rtl.css 1.08 kB 0 B
build/edit-navigation/style.css 1.08 kB 0 B
build/edit-post/index.js 304 kB 0 B
build/edit-post/style-rtl.css 5.63 kB 0 B
build/edit-post/style.css 5.63 kB 0 B
build/edit-site/index.js 17 kB 0 B
build/edit-site/style-rtl.css 3.06 kB 0 B
build/edit-site/style.css 3.06 kB 0 B
build/edit-widgets/index.js 11.7 kB 0 B
build/edit-widgets/style-rtl.css 2.45 kB 0 B
build/edit-widgets/style.css 2.45 kB 0 B
build/editor/editor-styles-rtl.css 537 B 0 B
build/editor/editor-styles.css 539 B 0 B
build/editor/index.js 45.3 kB 0 B
build/editor/style-rtl.css 3.8 kB 0 B
build/editor/style.css 3.79 kB 0 B
build/element/index.js 4.65 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 7.71 kB 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 711 B 0 B
build/keyboard-shortcuts/index.js 2.52 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.11 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.33 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/plugins/index.js 2.56 kB 0 B
build/primitives/index.js 1.41 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 13.9 kB 0 B
build/server-side-render/index.js 2.71 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 4.06 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@ZebulanStanphill ZebulanStanphill changed the title [WIP] InnerBlocks: introduce prop to specify component to wrap each block. [WIP] InnerBlocks: introduce prop to specify wrapper for each block. Jul 28, 2020
@noisysocks
Copy link
Member

noisysocks commented Jul 28, 2020

Thanks for exploring this!

Could you make something (e.g. the Navigation block) use this new API so that we can test how this approach affects things like drag and drop? I suspect there may be implications there.

I'm hesitant about how many if specified, then render something props there are on InnerBlocks. (tagName, passedProps, appenderTagName, and now itemWrapper.) It makes me think that we're using the wrong abstraction. For example, maybe if we made InnerBlocks a higher order component (or a hook?) we could lean on JSX to do this work for us.

// This is naive as it doesn't handle `key` and `ref` properly.
<InnerBlocks>
	{ ( blocks ) => (
		<ul>
			{ blocks.map( ( block ) => ( <li>{ block }</li> ) ) }
			<li><InnerBlocks.Appender /></li>
		</ul>
	) }
</InnerBlocks>

(But let's first test if the approach works before worrying about a nice API.)

@ZebulanStanphill
Copy link
Member Author

@noisysocks For testing purposes, I have now modified the Group block to wrap each of its children in a <section>. (I thought about modifying the Navigation Link block, but that one has a lot more special behavior going on that would complicate testing.) I'm building a plugin zip right now to test on one of my websites.

Your HOC idea looks pretty interesting. I'm not sure how to actually implement that concept, but it looks promising, so if you want to spin up a PR using that approach, please do so. I think something like that may be the right way to go.

@ZebulanStanphill ZebulanStanphill force-pushed the add/inner-blocks-child-wrapper-prop branch from 6b1d45a to 490dbcf Compare July 28, 2020 16:24
@ZebulanStanphill
Copy link
Member Author

I tested it, found some bugs, and fixed most of them.

Here's what works:

  • The save implementation works.
  • The parent selector button works.
  • The up/down movers work for the nested blocks (both single and multi-selection).

Here's what still doesn't:

  • Drag-and-drop into the parent between its children is not possible, though you can drop into the start of the Group block.
  • The native edit implementation still hasn't even been attempted yet.
  • No tests for this new API have been created.

Additionally, the only failing tests right now are caused by the temporary testing change made to the Group block's markup, so that's a good sign.

Notably, the up/down movers were already working prior to 490dbcf, but the animations were not. In fixing this, I ended up moving some of my wrapper logic from BlockList to BlockListBlock, where the block alignment wrapper is already handled. I figure that's probably where that logic belonged in the first place.

It should be noted that there is at least one major difference between the inner block item wrapper and the alignment wrapper: the former should not be considered part of the block, while the latter is (but should it)?

The alignment wrapper (for most blocks anyway) only exists in the editor, while the alignment classes are just applied to the block's normal root element in the save implementation. Ideally, the alignment should happen the same way in both implementations. Notably, the Image block uses a custom implementation which uses a wrapper for alignment in both the editor and the front-end.

To clarify, in my save implementation, the InnerBlocks child wrapper is placed outside of the child block's comment delimiters. This is because I don't consider the child wrapper to be a part of the child block itself. In contrast, I'd say an alignment wrapper is part of its associated block.

The editor situation is interesting since there are no comment delimiters, and as such, the lines between where a block starts and ends are a bit less obvious.

Any thoughts/opinions on this?

As for fixing the drag-and-drop issue, I'm not sure how to fix that yet. Any suggestions would be very welcome.

@ZebulanStanphill ZebulanStanphill force-pushed the add/inner-blocks-child-wrapper-prop branch 2 times, most recently from f11b79a to 57c0dfa Compare July 28, 2020 22:24
@ZebulanStanphill
Copy link
Member Author

Alright, drag-and-drop is fixed.

I've also attempted to make this work with React Native. I have no clue if the native implementation works or how to test it (I can't even get the normal wp-env set up on my computer), so if anyone could test that for me, that would be great.

@noisysocks
Copy link
Member

I tested the web version locally and it works pretty well! Nice job @ZebulanStanphill.

I think I'm happy with the approach. I like that it's a simple change, and the fact that we've encountered this problem in two seperate places reassures me that it's a problem worth solving at a framework level. I'd like it if the API were more flexible, as above, but nothing stopping us from iterating on that later seeing as this is all __experimental.

It's a bit of a paradigm shift, so I'd like to hear opinions from more @WordPress/gutenberg-core developers, particularly @ellatrix and @youknowriad, before proceeding. I note that it's linked to in the agenda of today's editor chat.

@ZebulanStanphill
Copy link
Member Author

I have now switched to a callback-based API, as suggested by @ellatrix. You can now do stuff like this:

<InnerBlocks
	__experimentalItemCallback={ ( item ) => (
		<>
			<section>{ item }</section>
			<p>You can do cool/weird stuff like this now.</p>
		</>
	) }
/>

One thing that I've discovered while working on this PR is that the position of the drag-and-drop indicator line isn't ideal. It appears before the block it will be dropping before, but it doesn't account for any extra markup added by this callback API, so if you're applying wrappers with a lot of top padding or appending stuff before each block, the drop-indicator won't appear in the right spot.

This is not a new problem; it exists in master as well, though it's only made obvious when dealing with large margins between blocks. I have no idea how to solve this little visual quirk.

Comment on lines +329 to +338
// Get the root elements of blocks inside the element, ignoring
// InnerBlocks item wrappers and the children of the blocks.
const blockElements = difference(
Array.from( element.current.querySelectorAll( '.wp-block' ) ),
Array.from(
element.current.querySelectorAll(
':scope .wp-block .wp-block'
)
)
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that this may not be the fastest way to do this. Here's an alternative implementation that I've considered, but I have no idea if it would be faster or slower:

function getBlockElements( node ) {
	const list = [];

	for ( const childNode of node ) {
		if ( childNode.classList.contains( 'wp-block' ) ) {
			list.push( childNode );
		} else if ( childNode.children.length > 0 ) {
			const grandchildren = getBlockElements( childNode );
			if ( grandchildren.length > 0 ) {
				list.push( ...grandchildren );
			}
		}
	}

	return list;
}

const blockElements = getBlockElements( element.current );

Copy link
Member

@noisysocks noisysocks Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably they're quite similar. When does this code run? Unless it's a very critical path (e.g. every keystroke, every block render) it's probably not worth worrying about too much.

You could always measure it using console.time().

console.time( 'getBlockElements' );
// Code goes here
console.endTime( 'getBlockElements' );

@@ -93,6 +93,7 @@ function BlockListBlock( {
toggleSelection,
index,
enableAnimation,
__experimentalCallback: wrapperCallback,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the __experimental prefix, this prop would just be named callback which is pretty ambiguous. Let's name it something like __experimentalBlockEditCallback.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to go with __experimentalRenderCallback. How's that?

packages/blocks/src/api/serializer.js Outdated Show resolved Hide resolved
@ZebulanStanphill ZebulanStanphill force-pushed the add/inner-blocks-child-wrapper-prop branch from b1c0e08 to 15e35db Compare August 6, 2020 16:02
@ZebulanStanphill
Copy link
Member Author

I realized that non-light blocks had the render callback applied inside their Block component rather than outside of it, so I pushed a fix for that. This also makes it so that the render callback wraps even the "Edit as HTML" form of blocks, which is neat.

@ZebulanStanphill ZebulanStanphill force-pushed the add/inner-blocks-child-wrapper-prop branch from 15e35db to 312f800 Compare August 6, 2020 16:09
@ZebulanStanphill ZebulanStanphill changed the title [WIP] InnerBlocks: introduce prop to specify wrapper for each block. InnerBlocks: introduce prop to specify render callback for each block. Aug 6, 2020
@ZebulanStanphill ZebulanStanphill marked this pull request as ready for review August 6, 2020 18:07
Fixes block moving animations for inner blocks.
This makes it possible to switch to a callback-based API, e.g. "itemWrapper={ ( item ) => <div>{ item }</div> }".
Edit implementation:

<InnerBlocks
    __experimentalItemCallback={
        ( item ) => <div>{ item }</div>
    }
/>

Save implementation:

<InnerBlocks.Content
    __experimentalItemCallback={
        ( item ) => <div>{ item }</div>
    }
/>
Fixes non-light blocks having the render callback applied inside the Block component rather than outside.

Also moves the "Edit as HTML" form of blocks inside the render callback.
getNearestBlockIndex now assumes all elements passed to it are the root elements of blocks; we filter out the other elements before passing the array to the function.
@ZebulanStanphill ZebulanStanphill force-pushed the add/inner-blocks-child-wrapper-prop branch from 86fd186 to 7342bc1 Compare August 15, 2020 03:40
@ZebulanStanphill ZebulanStanphill merged commit 48c4fb9 into master Aug 15, 2020
@ZebulanStanphill ZebulanStanphill deleted the add/inner-blocks-child-wrapper-prop branch August 15, 2020 04:55
@github-actions github-actions bot added this to the Gutenberg 8.8 milestone Aug 15, 2020
@ZebulanStanphill
Copy link
Member Author

As a follow-up: should we replace __experimentalAppenderTagName with an __experimentalAppenderCallback? Or should we just remove __experimentalAppenderTagName and make the appender use the same render callback as the blocks?

@ZebulanStanphill
Copy link
Member Author

Uh-oh. While testing this feature out in #24039, I think I've discovered a major flaw in the current implementation... namely that I only made changes to serialization code, and I never changed any of the parsing code. This means that inserting a block using this API will work fine in the editor, and even saving it and viewing it on the front-end will work. But if you leave the editor and then return, it won't be able to parse the blocks using InnerBlock render callbacks, and block validation will fail..

I'm not really sure how to fix this, but it's a serious bug that renders the entire feature unusable. Any technical help would be greatly appreciated.

@draganescu
Copy link
Contributor

Howdy @ZebulanStanphill. Is this feature used anywhere currently? In the navigation editor the problem ended up being solved differently. If there is no use case perhaps the best path for this API is to be removed via a revert?

@ZebulanStanphill
Copy link
Member Author

@draganescu The feature is currently being used in #24039, as I just mentioned.

@draganescu
Copy link
Contributor

Oops! 😊

@glendaviesnz
Copy link
Contributor

@ZebulanStanphill FYI - I am not too heavily invested in this approach in #24039, so if we need to back it out for now and explore alternatives that is fine, or also happy to wait until we get the parsing side sorted.

@ZebulanStanphill
Copy link
Member Author

@glendaviesnz I still think the render callback approach is the best one, because the only other solutions I am aware of are:

  • An intermediary "Gallery Item" block that's nothing more than a <li> that can have an Image block nested in it. This would complicate the process of adding another Image to the Gallery, and it would also complicate block navigation, unless we introduce a new API completely hide the block from the user-facing UI; and that would require progress on the PR to be halted anyway. See Blocks: Consider a "passthrough" supports for nested block wrappers #7694.
  • Conditional rendering of a <li> wrapper defined in the Image block. This feels really fragile and prone to complications with the placement of the wp-block-image class. It would also make it far more difficult for 3rd party image blocks to add support for being inserted into a core Gallery.

That said, it's probably best not to have a completely broken API in the code, even if it is experimental. And I am not currently aware of how to complete the implementation. So I've created #25196 to remove this API for now.

draganescu pushed a commit that referenced this pull request Sep 9, 2020
@nathanharper
Copy link

Is there any plan to revisit this? It would be really useful for a mixed-media gallery I'm trying to build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Nested / Inner Blocks Anything related to the experience of nested/inner blocks inside a larger container, like Group or P Needs Technical Feedback Needs testing from a developer perspective. [Type] Experimental Experimental feature or API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants