-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Introduce Child Blocks #5540
Comments
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.
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 |
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 |
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
I think this would be covered by the |
First thing this made me think of, is our ticket to let users insert images inline: #2043 (comment). 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 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.
We could probably deprecate
What should happen if the 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?
That tab is named Suggested now which I think works well for this 🙂 |
That's a good question. If we make Which makes me think that |
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:
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
|
OK, hear me out. The more I think about this more I think that a
ProposalMy current thinking is that we could enhance 1. A Photoset block that can contain only Image blocksTo whitelist blocks, we just specify a plain array: <InnerBlocks
allowedBlockNames={ [ 'core/image' ] }
/> 2. An Aside block that can contain any block except itselfTo blacklist blocks, we remove them from the passed array: <InnerBlocks
allowedBlockNames={ ( blockNames ) => without( blockNames, 'plugin/aside' ) }
/> 3. A Product block that can contain anything and Add To Cart blocksTo 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' );
} ); Weird diagramThe nice thing about this design, I think, is that it's composable: each 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 2 This filter could replace |
@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. |
@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:
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 @mtias mentioned:
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:
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. |
This is not accurate. From the documentation for
|
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 #5351 (comment) the introduction of a Columns / Column block distinction, where Columns sets as |
Ah! Good to know—this changes everything 😄
We could have it so that
Now that I know that there can only be one Still, it's confusing, I think, to have both |
I presume this is talking about how InnerBlocks are implemented with 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. |
Not per page, but per block. Each block creates its own inner blocks context. |
@noisysocks apart from the A
We are going to need both. |
How do we plan to solve the case when |
@gziolo specifying a relationship should trump not specifying a relationship. |
@gziolo I would say that in that situation, |
This was answered in #5540 (comment). In my proof of concept I made it so that, in this case, |
which is:
Yes, this is one way of solving it, but it makes it harder to understand how 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. |
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 ( Some illustrative examples1. A Photoset block that can contain only Image blocksregisterBlockType( 'acme/gallery', {
edit() {
return (
<InnerBlocks
allowedChildren={ {
'core/image': true,
'*': false,
} }
// Or, we can use the equivalent shorthand:
allowedChildren={ [ 'core/image' ] }
/>
);
},
} ); 2. An Aside block that can contain any block except itselfregisterBlockType( 'acme/aside', {
edit() {
return (
<InnerBlocks
allowedChildren={ {
'acme/aside': false,
'*': true,
} }
/>
);
},
} ); 3. An Add To Cart block that can only be added to Product blocksregisterBlockType( 'acme/add-to-cart', {
allowedParents: {
'acme/product': true,
'*': false,
},
// Or, we can use the equivalent shorthand:
allowedParents: [ 'acme/product' ],
} ); Some formality
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,
Besides that, I think we should have hooks on the three levels of restriction that allow the settings to be changed. 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. |
You could also, with the advanced allow list syntax, set 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 |
This would be a very useful feature! Can I emulate this already? Edit: Related issue: #6607 |
You can use the |
@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. |
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.
Justification
Let's consider a block called
Product
that represents an item with various sub-elementsPrice
,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.Implementation
The addition to the block API would be something like the following:
A parent block would, in turn, be able to define these children as defaults, just like templates and
InnerBlocks
works.Inserter
The inserter would have to be aware of the context to include (and exclude) these additional blocks when the occasion is right.
Consideration
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.The text was updated successfully, but these errors were encountered: