Extend the concept of inner blocks to support a more direct relationship between sets of blocks. This new addition of parent–child would be a subset of the inner block functionality. The premise is that certain blocks _only_ make sense as children of another block.
Let's consider a block called Product
that represents an item with various sub-elements Price
, Name
, Add to Cart
, etc. These are treated as blocks that the developer wants to _define_ but let users manipulate directly. They also want to use the native inner blocks functionality instead of recreating its interactions in ways that would be inconsistent.
The key difference with regular blocks is that these blocks (price, name, cart, etc) should not appear in the main inserter _unless_ the user is currently within the parent Product
block. In other words, these inner blocks are registered to _only be available for inserting within a product block_, and not elsewhere.
The addition to the block API would be something like the following:
const name = 'plugin/product-price';
const settings = {
// array property with allowed parents
parent: [ 'plugin/product' ]
};
A parent block would, in turn, be able to define these children as defaults, just like templates and InnerBlocks
works.
The inserter would have to be aware of the context to include (and exclude) these additional blocks when the occasion is right.
There are similarities here with the idea of restricting a block to a CPT. In that case, the CPT is effectively the "parent" property. We might find a way to combine both — perhaps parent: [ 'post', 'page', 'plugin/product' ]
allows to specify both post types and blocks in the same mechanism. Given blocks require a slash for namespace, we might be able to split them through that implicit mark.
Very nice. I can think of a lot of uses for this. Will encourage parent blocks with more standard UI, and will reduce block mental overload.
Given blocks require a slash for namespace, we might be able to split them through that implicit mark
Eh, if it is split out, it is immediately obvious what is going on.
const name = 'plugin/product-price';
const settings = {
// array property with allowed parents
parent: [ 'plugin/product' ],
postType: [ 'post', 'page' ]
};
I'm thinking that the inserter should prioritize child-blocks that explicitly mention a parent, when inserting into that parent. So adding to Product
would show you Price
Product Name
, Add to Cart
before anything else.
This will greatly improve the granularity with more specific blocks. One additional thing to consider I could think of: Child blocks of a product may wanna have the requirement of either a product block as parent or as a regular (non-child) block in a product
post type. It may be too specific, but I think it is a common scenario as well (think about a dedicated product page vs embedding a product in another piece of content).
I'm thinking that the inserter should prioritize child-blocks that explicitly mention a parent, when inserting into that parent.
Yes, this was what I had in mind too. We probably should rename the first tab of the inserter to something like "Common", since it now takes both frequency and recency into account, and with this further contextual addition it would still be applicable as "common for the current context". cc @jasmussen
It may be too specific, but I think it is a common scenario as well (think about a dedicated product page vs embedding a product in another piece of content).
I think this would be covered by the allowedBlocks
part of template declaration (inverts control from child to parent). See https://github.com/WordPress/gutenberg/pull/5452
Yes, this was what I had in mind too. We probably should rename the first tab of the inserter to something like "Common", since it now takes both frequency and recency into account, and with this further contextual addition it would still be applicable as "common for the current context".
First thing this made me think of, is our ticket to let users insert images inline: https://github.com/WordPress/gutenberg/issues/2043#issue-245834556.
Although there are nuances between this ticket and inserting images or other inline elements into paragraphs, it makes a lot of sense to think about the insertion UI in a cohesive way here.
Definitely, the only difference is that other blocks are also valid in this case, so it's less about an entirely different mode and more about prioritizing child blocks somehow.
For symmetry with existing functions like register_taxonomy
's second argument $object_type
, we could consider aligning this as an argument of the registerBlockType
function itself:
registerBlockType( 'plugin/product-price', [
'plugin/product',
'post',
'page',
], {
// ...
} );
I'm going to take this on. There's some slight overlap between this feature and #5452 so first I'll help @jorgefilipecosta get that merged in. I'll then work on a quick proof of concept to serve as the base for a conversation about how we go about implementing this and the exact API.
There are similarities here with the idea of restricting a block to a CPT. In that case, the CPT is effectively the "parent" property. We might find a way to combine both — perhaps parent: [ 'post', 'page', 'plugin/product' ] allows to specify both post types and blocks in the same mechanism.
We could probably deprecate isPrivate
in lieu of this, too. If one restricts a block from being inserted into _everything_, then the block essentially becomes private.
The inserter would have to be aware of the context to include (and exclude) these additional blocks when the occasion is right.
What should happen if the allowedBlockNames
property of a parent block (added in #5452) conflicts with the parent
property of one of its child blocks?
For example, if we had:
// product.js
const name = 'plugin/product';
const settings ={
title: 'Product',
edit() {
return (
<InnerBlocks
allowedBlockNames={ [ 'core/paragraph', 'plugin/product-price' ] }
/>
);
},
...
}
// product-add-to-cart.js
const name = 'plugin/product-add-to-cart';
const settings = {
title: 'Add to cart',
parent: [ 'plugin/product' ],
...
}
Can a user add a _Add to cart_ block to the _Product_ block?
We probably should rename the first tab of the inserter to something like "Common", since it now takes both frequency and recency into account, and with this further contextual addition it would still be applicable as "common for the current context".
That tab is named _Suggested_ now which I think works well for this 🙂
What should happen if the allowedBlockNames property of a parent block (added in #5452) conflicts with the parent property of one of its child blocks?
That's a good question. If we make allowedBlockNames
win, it means that there's no easy way for a 3rd party block to become "available" in another 3rd party block.
Which makes me think that parent: [ 'plugin/product' ]
should work as an addition to allowedBlockNames definition (unless filtered out).
I'm very excited about this. I think that this fills a lot of the gaps in templates, that I talked with @gziolo about awhile back. For example, what if we want some blocks in a template to be required, but can be moved, or not required but if they are present they must be the first block, etc.
From @noisysocks comments:
Can a user add a Add to cart block to the Product block?
I think it would be great if in the parent block, I could define basically like @noisysocks but also whitelist and blacklist child blocks.
So in this updated pseudo-code, the Add to Cart block can be added to Product block because it's in the array used for allowedChildren
, but it is not a there by default, because it's not part of the array used for defaultChildern
.
const defaultChildren = [ 'core/paragraph', 'plugin/product-price' ];
const settings ={
title: 'Product',
edit() {
return (
<InnerBlocks
allowedBlockNames={ defaultChildren }
/>
);
},
defaultChildern: defaultChildren, //array of blocks to show by defauls.
allowedChildern: [ 'plugin/product-add-to-cart' ] //array of blocks that can be added. Could be false to prevent adding blocks. Maybe true to allow any type.
forbiddenChildern: [ 'core/heading' ] //array of blocks that can not be added.
}
OK, hear me out.
The more I think about this more I think that a parent
property is the wrong approach. A few reasons:
<InnerBlocks>
components. This will be important as we head towards allowing folks to build complex layout blocks, e.g. a block that defines a content region, a sidebar region, and a footer region.parent
, allowedBlockNames
and allowed_block_types
all represent the same concept: restricting what blocks can be inserted into a (sub)tree of blocks. But it's hard to reason about these three concepts simultaneously, because whereas allowedBlockNames
takes a _top down_ view of the block tree, parent
takes a _bottom up_ view.My current thinking is that we could enhance allowedBlockTypes
to accept a function. To illustrate, let's run through some scenarios.
To whitelist blocks, we just specify a plain array:
<InnerBlocks
allowedBlockNames={ [ 'core/image' ] }
/>
To blacklist blocks, we remove them from the passed array:
<InnerBlocks
allowedBlockNames={ ( blockNames ) => without( blockNames, 'plugin/aside' ) }
/>
To allow additional blocks, we add them to the passed array:
<InnerBlocks
allowedBlockNames={ ( blockNames ) => [ ...blockNames, 'plugin/add-to-cart' ] }
/>
We can go further and tell Gutenberg that we think it ought to put the _Add To Cart_ block in the Suggested tab1:
<InnerBlocks
allowedBlockNames={ ( blockNames ) => [ ...blockNames, 'plugin/add-to-cart' ] }
suggestedBlockNames={ [ 'plugin/add-to-cart' ] }
/>
Going further still, we can tell Gutenberg to not allow the _Add To Cart_ block to be inserted at the root level2:
addFilter( 'post.allowedBlockNames', 'plugin/allowed-block-names', ( blockNames ) => {
return without( blockNames, 'plugin/add-to-cart' );
} );
The nice thing about this design, I think, is that it's composable: each allowedBlockTypes()
function takes the result of its parent's allowedBlockTypes()
. You can e.g. insert an _Add To Cart_ block in your _Aside_ block so long as the _Aside_ block is nested within a _Product_ block.
It's easy to reason about because you can picture your post as a tree of blocks, with the list of allowed block types "flowing" down from the root:
1 We could maybe infer this automatically based on what allowedBlockNames()
receives versus what it returns.
2 This filter could replace isPrivate
, e.g. core/block
would be excluded by default. It could also replace or compliment the allowed_block_types
PHP filter.
@noisysocks agreed a filter function is the best solution.
Regarding top down or bottom up I think the correct solution involves doing both:
InnerBlocks - should be able to control what content they contain - and they should get the final say - preferably with a simple way to add preference (As discussed above.)
Blocks in general - should be able to decide themselves whether they get included in the inserter given their current context. I haven't looked at how that could happen (or if its viable) but that seems like it gives you the most freedom.
Probably goes without saying but I would continue to keep both concepts separate they're similar in that they deal with the inserter but they goals are quite dissimilar.
Innerblock filters are more about controlling content. Where block level filters are more about creating context aware blocks.
Total hunch but I suspect doing block level filtering would only including changing isPrivate to an function and maybe renaming the name. Quite easy to handle any existing isPrivate settings that way as well.
OK, hear me out.
@noisysocks I am still processing all of this but tend to agree with the general approach. I'm going to get a little philosophical because I do that sometimes. It's helpful for me but hopefully not just for me.
A top-down approach is natural like a house is built on a foundation. It allows a truth to naturally inform the system. In this case, that means allowing a parent block implementation to inform its contents. A bottom-up approach cannot wield the same authority because it does not form a foundation.
A bottom-up approach can co-exist with a top-down approach but only with authority granted by the top-down approach, and for me, it is easier to think about a top-down-only approach, similar to how React is easier to think about with unidirectional data flow.
Regarding mechanism:
My current thinking is that we could enhance
allowedBlockTypes
to accept a function.
This sounds good to me because it gives us the ability to use logic rather than programming-by-data structure using lists, but it seems like we need a way to express the truth of allowedBlockTypes
on the server since it is the source of truth (when Gutenberg is used within WordPress). I'm wondering whether this means we need to continue expressing allowed block types with lists.
@mtias mentioned:
If we make
allowedBlockNames
win, it means that there's no easy way for a 3rd party block to become "available" in another 3rd party block.
With a top-down-only approach, I am thinking that a plugin adding a 3rd-party block would need to use a filter hook to add the 3rd-party block as an allowed block.
Regarding composability:
The nice thing about this design, I think, is that it's composable: each allowedBlockTypes() function takes the result of its parent's allowedBlockTypes(). You can e.g. insert an Add To Cart block in your Aside block so long as the Aside block is nested within a Product block.
What does this mean for a hypothetical Slideshow block that can contain only Slide blocks? We would want Slide blocks to allow many block types, but it sounds like that would be limited by the restriction on Slideshow.
Broadly restricting a block's parent ignores that a parent block can have multiple
<InnerBlocks>
components.
This is not accurate. From the documentation for InnerBlocks
:
_Note_: A block can render at most a single
InnerBlocks
andInnerBlocks.Content
element inedit
andsave
respectively.
The idea of gradually filtering down a list of allowable blocks is a bit problematic to me. @brandonpayton highlighted one example with slideshow. The other is even Columns, which are admittedly quite broken without a wrapping element, and where I considered in https://github.com/WordPress/gutenberg/issues/5351#issuecomment-375795651 the introduction of a Columns / Column block distinction, where Columns sets as allowedBlockTypes
only Column, but Column could contain anything. Whether or not this is the approach followed to resolve #5351, it seems a reasonable consideration to allow. On the surface, doesn't seem that a block's restrictions on inner blocks should apply to its grandchildren or great-grandchildren.
_Note_: A block can render at most a single
InnerBlocks
andInnerBlocks.Content
element inedit
andsave
respectively.
Ah! Good to know—this changes everything 😄
What does this mean for a hypothetical Slideshow block that can contain only Slide blocks? We would want Slide blocks to allow many block types, but it sounds like that would be limited by the restriction on Slideshow.
We could have it so that allowedBlockTypes={ true }
allows all block types to be nested within the parent. This matches how the property currently works.
On the surface, doesn't seem that a block's restrictions on inner blocks should apply to its grandchildren or great-grandchildren.
Now that I know that there can only be one InnerBlocks
per block I'm more favourable to the originally suggested approach where a block can specify during registration what blocks it can be inserted into.
Still, it's confusing, I think, to have both allowedBlockTypes
on the parent and e.g. allowedParents
on the child. I would prefer that our API has as few concepts to do with limiting where blocks can be inserted as possible. Any thoughts or ideas?
cc. @jorgefilipecosta
Note: A block can render at most a single InnerBlocks and InnerBlocks.Content element in edit and save respectively.
I presume this is talking about how InnerBlocks are implemented with withContext
?
That looks like we can only use a single InnerBlocks per page?
Is that actually intended?
Because I suspect people will work around it eventually.
That looks like we can only use a single InnerBlocks per page?
Not per page, but per block. Each block creates its own inner blocks context.
@noisysocks apart from the InnerBlocks
clarification, I wanted to add that, even though the result is similar (filtering down which blocks are available within an InnerBlocks
area) the intention and expressiveness are different.
A parent
property is a child declaring what their context is and saying "I should not be treated as a root block" or as part of the default set of available inner blocks in other blocks (like Columns). This has the advantage of not depending on filtering the parent's functionality — the parent doesn't need to have any specific knowledge of this child block.
allowedBlocks
, however, is about the parent restricting what can be inserted within itself. It's an API the block author can directly control.
We are going to need both.
How do we plan to solve the case when Parent Block A
allows only Block B
as children and Child Block C
allows only Parent Block A
as the parent?
@gziolo specifying a relationship should trump not specifying a relationship.
@gziolo I would say that in that situation, Child Block C
would simply be unable to be inserted anywhere. To get around this (in the context of a plugin/theme adding a child block to a block from WordPress core or another plugin/theme that usually only has one allowed child), you should be able to explicitly override the list of allowed children of a block just like you can override the edit/save functions of any block.
How do we plan to solve the case when Parent Block A allows only Block B as children and Child Block C allows only Parent Block A as the parent?
This was answered in https://github.com/WordPress/gutenberg/issues/5540#issuecomment-380816384. In my proof of concept I made it so that, in this case, Block C
is insertable into Parent Block A
. The plugin I created to help test the proof of concept has an example of this.
This was answered in #5540 (comment)
which is:
That's a good question. If we make
allowedBlockNames
win, it means that there's no easy way for a 3rd party block to become "available" in another 3rd party block.Which makes me think that
parent: [ 'plugin/product' ]
should work as an addition to allowedBlockNames definition (unless filtered out).
Yes, this is one way of solving it, but it makes it harder to understand how allowedBlockNames
property work. In that case it should be really allowedBlockNamesWithoutChildrenThatOptInExplicitly
:)
I think the same question applies to Templates, we need to have well-defined priorities for every solution that wants to modify what should be allowed in a given context.
There is one drawback I can envision with this approach. When a site owner would want to disallow multiple individual Child blocks to be exposed in the inserter of the given parent block, they would have to update all such blocks one by one. We need to keep that in mind that it is going to be easier for those who create Child blocks, but not always to those who want to keep parent blocks isolated.
There is one drawback I can envision with this approach. When a site owner would want to disallow multiple individual Child blocks to be exposed in the inserter of the given parent block, they would have to update all such blocks one by one.
Been thinking about this.
To handle all of these cases we're identifiying, our API needs to be expressive enough such that a block can specify:
To this end, I think what would work is if we introduce the concept of an allow list. This is an object that maps block types to whether or not they are explicitly allowed as a parent or child. A wildcard (*
) case determines whether or not parents or children are allowed _in general_.
registerBlockType( 'acme/gallery', {
edit() {
return (
<InnerBlocks
allowedChildren={ {
'core/image': true,
'*': false,
} }
// Or, we can use the equivalent shorthand:
allowedChildren={ [ 'core/image' ] }
/>
);
},
} );
registerBlockType( 'acme/aside', {
edit() {
return (
<InnerBlocks
allowedChildren={ {
'acme/aside': false,
'*': true,
} }
/>
);
},
} );
registerBlockType( 'acme/add-to-cart', {
allowedParents: {
'acme/product': true,
'*': false,
},
// Or, we can use the equivalent shorthand:
allowedParents: [ 'acme/product' ],
} );
[ 'foo', 'bar' ]
is shorthand for { 'foo': true, 'bar': true, '*': false }
.
true
and undefined
are shorthand for { '*': true }
. false
is shorthand for { '*': false }
.
The logic for determining whether or not a child can be inserted into a parent is:
function isChildAllowedInParent(
parentType,
parentAllowList,
childType,
childAllowList
) {
// If the parent has an explicit allow/disallow, use it
if ( childType in parentAllowList ) {
return parentAllowList[ childType ];
}
// If the child has an explicit allow/disallow, use it
if ( parentType in childAllowList ) {
return childAllowList[ parentType ];
}
// Otherwise, allow if both blocks implicitally allow
return parentAllowList[ '*' ] && childAllowList[ '*' ];
}
Hi, @noisysocks,
I really like the directions we are taking and the solution you proposed in https://github.com/WordPress/gutenberg/issues/5540#issuecomment-386192518.
In my option we should, in fact, have three levels of restriction:
Besides that, I think we should have hooks on the three levels of restriction that allow the settings to be changed.
So, if I'm creating a block equivalent to 'acme/product' and I also want to support acme/add-to-cart inside my block, I can hook into registerBlockType and change the way acme/add-to-cart is registered to allow it to be nested inside my block. I think there will be no additional work here as we can use the existing hooks to extend the block registration.
The contrary should also be possible if a parent block sets some restriction and I'm creating a block equivalent to one of the child blocks, I should be able to use a hook and allow my block to be nested inside. We will need to create a new hook for this.
So, if I'm creating a block equivalent to 'acme/product' and I also want to support acme/add-to-cart inside my block, I can hook into registerBlockType and change the way acme/add-to-cart is registered to allow it to be nested inside my block.
You could also, with the advanced _allow list_ syntax, set allowedChildren={ { 'acme/add-to-cart': true, '*': true } }
on the <InnerBlocks>
in acme/product
.
But yes, I agree, hooks would provide a good API for doing really advanced (e.g. programatic) things with these allow lists.
It would be great to implement the same syntax on PHP side for allowed_block_types
. At the moment it supports only whitelisting by providing the list of block names.
This would be a very useful feature! Can I emulate this already?
When this is not possible yet, can I repeat a component inside that can hold blocks by itself?
Edit: Related issue: https://github.com/WordPress/gutenberg/issues/6607
When this is not possible yet, can I repeat a component inside that can hold blocks by itself?
You can use the InnerBlocks
component in your blocks.
@mtias: Right. But for the slider block (see referenced issue), how can an user simply click inside for a new slide(slide, not slider) block? Currently the user can click on a slider block (which nests a new slider) or an image, but the other elements aren't offered.
Also the amount of available sub-blocks has to be configured by user first, it doesn't just grow like the top level Gutenberg editor.
Most helpful comment
OK, hear me out.
The more I think about this more I think that a
parent
property is the wrong approach. A few reasons:<InnerBlocks>
components. This will be important as we head towards allowing folks to build complex layout blocks, e.g. a block that defines a content region, a sidebar region, and a footer region.parent
,allowedBlockNames
andallowed_block_types
all represent the same concept: restricting what blocks can be inserted into a (sub)tree of blocks. But it's hard to reason about these three concepts simultaneously, because whereasallowedBlockNames
takes a _top down_ view of the block tree,parent
takes a _bottom up_ view.Proposal
My current thinking is that we could enhance
allowedBlockTypes
to accept a function. To illustrate, let's run through some scenarios.1. A Photoset block that can contain _only_ Image blocks
To whitelist blocks, we just specify a plain array:
2. An Aside block that can contain any block _except_ itself
To blacklist blocks, we remove them from the passed array:
3. A Product block that can contain anything _and_ Add To Cart blocks
To allow additional blocks, we add them to the passed array:
We can go further and tell Gutenberg that we think it ought to put the _Add To Cart_ block in the Suggested tab1:
Going further still, we can tell Gutenberg to not allow the _Add To Cart_ block to be inserted at the root level2:
Weird diagram
The nice thing about this design, I think, is that it's composable: each
allowedBlockTypes()
function takes the result of its parent'sallowedBlockTypes()
. You can e.g. insert an _Add To Cart_ block in your _Aside_ block so long as the _Aside_ block is nested within a _Product_ block.It's easy to reason about because you can picture your post as a tree of blocks, with the list of allowed block types "flowing" down from the root:
1 We could maybe infer this automatically based on what
allowedBlockNames()
receives versus what it returns.2 This filter could replace
isPrivate
, e.g.core/block
would be excluded by default. It could also replace or compliment theallowed_block_types
PHP filter.