-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Try a toolbar that affects multiple selected blocks #7635
Conversation
core-blocks/paragraph/index.js
Outdated
@@ -314,6 +322,8 @@ export const settings = { | |||
|
|||
attributes: schema, | |||
|
|||
toolbar, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we introduce a toolbar
API like this (maybe controls
is better thought), Can we just automatically apply it to the block edit
function if available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this idea. I'll see what I can get working 👍
core-blocks/paragraph/index.js
Outdated
setAttributes( { align: nextAlign } ); | ||
} } | ||
/> | ||
{ toolbar( this.props, this.state ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the second argument is useless here? Since the state is local to a block I believe we shouldn't use it in an API that can be common to multiple components. If block authors need state shared between edit
and toolbar
, it doesn't make sense to use toolbar
, they'll just embed BlockControls
in edit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@youknowriad I've moved |
And now the multi-block toolbar shows the number of selected blocks, as @mtias originally requested :) |
See #1811 and #3535 for previous discussion. One of the problems with having a toolbar inside the first block wrapper is that it cannot be made sticky for the whole selection. I'm not sure what the solution here could be other than wrapping the selection in a There's also the area in the block inspector which this PR could implement: |
core-blocks/paragraph/index.js
Outdated
@@ -314,6 +317,8 @@ export const settings = { | |||
|
|||
attributes: schema, | |||
|
|||
controls, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last time there was some discussion for keeping the controls in the edit function: #3535 (comment). Insure if @aduth and @mtias still feel this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does get tricky if you want to reuse the same controls for multiple blocks, mostly because you'd create the components with the instance's setAttributes
, and I couldn't see a way of nicely injecting a setAttributes
that affected multiple blocks that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In passing: I still don't particularly like this being part of the block API, when otherwise we've included it (and other block controls) as part of the block's own edit
rendering. Including it in the block API seems more of a means to an end (being multi-block control) than being strictly "correct". Of course, I don't have a great idea in mind for working around this, aside from... not doing multi-toolbars 😄 Perhaps there is something where we could fake the render once of a given consistent set of selected blocks, inspect the result to see if there are block controls, and use that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried something similar to the fake render, grabbing the controls from <BlockControls>
and reusing them, but their onClick
handlers were bound to functions that had the setAttributes
and values from this.props
, and I couldn't make them reusable. That's been what forced me into putting them into the block API - I can grab the controls from the slot, but they're bound to a single block instance.
So while I would rather have them in the edit function too, I can't see a way of reusing them for the multi block toolbar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, we're going to come up against the same thing for the toolbar
too. So if there is a way, we should talk about it as much as possible here so we have the right approach :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it could be a separate API for blocks to register themselves as being capable of having multi-select capabilities? My main concern is avoiding introducing an inconsistency both in controls themselves, but also in the general rendering approach to block UI pieces (and the responsibilities of edit
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've also blurred the line between how individual block units relate to one another. The original API was never meant to really support this sort of "apply behaviors to multiple instances", it was designed to support a single instance at a time. Bolting it on at this point will make for something of a Frankenstein API.
To be fair, I also lean more on the end of: If we can't make it work and have a consistent approach to block independence vs. interdependence, it shouldn't be included as a feature at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree there. It's a big missing feature for end users, and shouldn't be skipped because of technical purity. Having said that, I do agree that having 90% of the API able to use this
and the rest not able to, feels quite wrong!
Perhaps we have block controls as something which decorates a block's settings, to keep the approaches distinct from each other?
From a dev perspective, something like:
// controls.js
export const controls = {
toolbar: function( attributes, setAttributes ) {
// ... do your instance agnostic toolbar controls thing there
},
inspector: function( attributes, setAttributes ) {
// ... and inspector things here
}
}
// index.js
import { withControls } from '../../blocks/api';
import controls from './controls';
const settings = withControls( controls, {
// block settings here, using `this` happily
} );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking more into what we can do here today. I feel like I should say, I agree that this shouldn't go in settings, and looking more at how block registration works, I don't think withControls
fits either. Perhaps if we define controls
as well as settings
in a block, that could be passed to registerBlockType
, and because it's not inside edit
, there's no danger of trying to use this
in a context where it shouldn't be used.
I'll put something together for comment :)
const attributes = {}; | ||
// Reduce the selected block's attributes, so if they all have the | ||
// same value for an attribute, we get it in the multi toolbar attributes. | ||
for ( let i = 0; i < multiSelectedBlocks.length; i++ ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it not possible to use [].reduce here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The special case for index 0 made doing it this way more readable, to my eyes anyway
@aduth so, I've tried something slightly different :) Outside of |
Update after some discussion with the core team : Adding I'm going to carry on with an experiment to see if block toolbar controls can be multi-block aware, so a block developer doesn't have to think about anything more than putting controls as children of a |
5955c57
to
f6de2b9
Compare
New shiny code with a new approach! All changed, rebased, and squashed. This is a way of having multi block aware controls without having to repeat control definitions, or change the API. A new HOC, Block developers are able to specify controls that apply to single blocks only using Example from the paragraph block:
In this example, the alignment toolbar will be used for single blocks and multiple blocks, there's no need to define it twice. The new |
f6de2b9
to
d412ad3
Compare
Rebased for the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not taken this for a spin yet, but am curious: Is this expected to work as-is? I'm really liking the new (limited) impact on block API!
core-blocks/paragraph/index.js
Outdated
return ( | ||
<Fragment> | ||
<BlockControls> | ||
<AlignmentToolbar | ||
<MultiBlockControls> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this still support the use-case of the single block selection?
It also prompts me to think: Can we just build-in multi block controls behavior to <BlockControls />
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and no, respectively :)
There needs to be some way to show just the multi block controls when multiple blocks are selected. With this approach, it's two slots, one for multi- and one for single- controls. Both are rendered when only one block is selected, and only the multi slot is rendered when we have multiple blocks selected.
I toyed with having some kind of registry, or attribute on elements, but those approaches didn't seem as clean and as obvious to the block developer.
This PR won't work as-is with the current state of master, but I'm really after feedback on the approach at the moment before I go rebasing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There needs to be some way to show just the multi block controls when multiple blocks are selected. With this approach, it's two slots, one for multi- and one for single- controls. Both are rendered when only one block is selected, and only the multi slot is rendered when we have multiple blocks selected.
So is this just a complexity of the implementation? Or is it conceptually important that, as part of the edit
interface, a distinction is made between single- and multi- selection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little from column A and a little from column B.
I'd say that allowing the distinction to be made in edit
allows block developers to specify if they want controls to apply to multiple blocks or not, however, do they need to? Is a block developer ever going to say, "I have this control that can update multiple blocks at once, and I really don't want the user to do that!"
Probably not.
I'd be very happy to hear ideas on how we could flag up controls that should apply to multiple blocks and therefore sort out the rendering purely with <BlockControls />
. All my previous attempts at that smelled wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing the implementation:
const MultiBlockAlignmentToolbar = withMultiBlockSupport( AlignmentToolbar, 'align' );
which effectively adds the attributeName
for the toolbar control behind the scenes. I would bet that it is possible to express all this code as follows:
<BlockControls>
<BlockAlignmentToolbar
name="align"
multi={ true }
value={ align }
onChange={ onChange }
/>
</BlockControls>
It would require some refactoring for BlockControls
which is wrapped with ifBlockEditSelected
HOC. It would have to support also the case when block is multi selected and have multi
set to true
. Finally, it would also have to handle the case when multi
is set to false
and block is multi selected - not render such control at all.
Still, the biggest challenge would be to iterate over those multiplied controls filled into slot and apply some modifications to the virtual dom tree which would allow to group them, render only one but trigger update for all of them. Basically, replicate what withMultiBlockSupport
does.
In the future, we could go even further and hide the need for using value
and onChange
props since they are usually repeatable so the can be derived from the attribute name as explored in #6705.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is heading in the right direction and is going to be a super useful feature. It is very complex one, but very powerful for all plugin developers so let's make sure that public API is as simple as possible.
It would be lovely if we could explore what @aduth suggested:
Can we just build-in multi-block controls behavior to
<BlockControls />
?
I left my comment about that.
core-blocks/paragraph/index.js
Outdated
return ( | ||
<Fragment> | ||
<BlockControls> | ||
<AlignmentToolbar | ||
<MultiBlockControls> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing the implementation:
const MultiBlockAlignmentToolbar = withMultiBlockSupport( AlignmentToolbar, 'align' );
which effectively adds the attributeName
for the toolbar control behind the scenes. I would bet that it is possible to express all this code as follows:
<BlockControls>
<BlockAlignmentToolbar
name="align"
multi={ true }
value={ align }
onChange={ onChange }
/>
</BlockControls>
It would require some refactoring for BlockControls
which is wrapped with ifBlockEditSelected
HOC. It would have to support also the case when block is multi selected and have multi
set to true
. Finally, it would also have to handle the case when multi
is set to false
and block is multi selected - not render such control at all.
Still, the biggest challenge would be to iterate over those multiplied controls filled into slot and apply some modifications to the virtual dom tree which would allow to group them, render only one but trigger update for all of them. Basically, replicate what withMultiBlockSupport
does.
In the future, we could go even further and hide the need for using value
and onChange
props since they are usually repeatable so the can be derived from the attribute name as explored in #6705.
FWIW, and along the same lines as mentioned in #7635 (comment), in discussing this with @mtias there's may be an argument to be made that the controls for single selection may not be strictly the same as the controls for multi-selection of a block. I'm not entirely sure what the use-case would be. At the very least then, I think we'd want the two sets of controls to be insulated from one another (i.e. MultiControls doesn't also implement singular controls, even if that means duplicating). |
Does this mean that the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that the withMultiBlockSupport HOC is a no-go, or could we provide that for devs to use with simple controls where it fits nicely
I'm not entirely clear its purpose, and while I think some "magic" for making attribute assignment easier would be a nice addition (explored also in #6705 and #7352), it's important that it's introduced consistently. If a control assigns attribute values for multi blocks, is there a reason it shouldn't for singular? What controls does this cover? What about support for non-standard attribute names?
I wonder if <MultiBlockControls>
could provide context by a render prop function which exposes attributes
and setAttributes
which read from / apply to all blocks in the multi-selection.
<MultiBlockControls>
{ ( { attributes, setAttributes } ) => (
<BlockAlignmentToolbar
value={ attributes.align }
onChange={ ( nextAlign ) => {
setAttributes( { align: nextAlign } );
} }
/>
) }
</MultiBlockControls>
There's a potential issue here in variable shadowing.
*/ | ||
export const withMultiBlockSupport = ( component, attributeName ) => createHigherOrderComponent( ( OriginalComponent ) => { | ||
const multSelectComponent = ( props ) => { | ||
const newProps = { ...props }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shallow clone may be costly and unnecessary when we don't have a multi selection. I think it should only be done inside the condition.
* @return {Component} Component that can handle multple selected blocks. | ||
*/ | ||
export const withMultiBlockSupport = ( component, attributeName ) => createHigherOrderComponent( ( OriginalComponent ) => { | ||
const multSelectComponent = ( props ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: mult
-> multi
* | ||
* @return {Component} Component which renders only when the BlockEdit is selected or it is the first block in a multi selection. | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This newline shouldn't be here, at risk of the JSDoc not being associated with the function.
* @return {Component} Component which renders only when the BlockEdit is selected or it is the first block in a multi selection. | ||
*/ | ||
|
||
const isFirstOrOnlyBlockSelectedHOC = createHigherOrderComponent( ( OriginalComponent ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re: Naming, I tend to encourage avoiding the "HOC" terminology because it's not always obvious to new developers. We already have a convention around the "with" prefix, that I think we could use here as well.
return ( props ) => { | ||
return ( | ||
<Consumer> | ||
{ ( { isSelected, clientId } ) => ( isSelected || ( clientId === props.getFirstMultiSelectedBlockClientId && props.allSelectedBlocksOfSameType ) ) && ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line too long.
allSelectedBlocksOfSameType, | ||
}; | ||
} ), | ||
] )( isFirstOrOnlyBlockSelectedHOC( component ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what compose
serves to eliminate, to rewrite:
a( b( c( d( Component ) ) ) );
...as:
compose( [ a, b, c, d ] )( Component );
In other words, isFirstOrOnlyBlockSelectedHOC
could be added as an entry of the compose
argument array.
* | ||
* @return {*} Reduced value of attribute. | ||
*/ | ||
function reduceAttribute( multiSelectedBlocks, attributeName ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a function called reduceAttribute
, why not just Array#reduce
? 😄
return multiSelectedBlocks.reduce( ( attribute, block, i ) => {
const block = multiSelectedBlocks[ i ];
if ( block.attributes[ attributeName ] === attribute || 0 === i ) {
return block.attributes[ attributeName ];
}
return undefined;
} );
if ( block.attributes[ attributeName ] === attribute || 0 === i ) { | ||
attribute = block.attributes[ attributeName ]; | ||
} else { | ||
attribute = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW if the idea is to abort with undefined
if we encounter a different attribute, this could be a return;
statement, which would help avoid needing to iterate the rest of the array.
(This optimization won't be possible if converted to Array#reduce
)
core-blocks/paragraph/index.js
Outdated
return ( | ||
<Fragment> | ||
<BlockControls> | ||
<AlignmentToolbar | ||
<MultiBlockControls> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm how we want to proceed with this...
Does that sound like a plan? Anything that needs to be corrected or added? |
Could you elaborate on what this means? I don't recall where this was discussed. Otherwise seems reasonable. The point on variable shadowing will probably become painfully obvious, and makes me wonder if there are other avenues to explore (or patterns to establish at least) for addressing. More a practical concern than fundamental issue, though one could argue we should do better establishing that in these examples, |
@mapk and @youknowriad - this is another interesting feature that should be considered as something to work on in phase 2. @notnownikki do you plan to continue working on it? It looks like it wasn't updated since August. |
Yes I'd like to continue with this. There may be better ways to achieve it now, I'd like to get eyes on it for design and code advice as part of phase 2 if possible. |
I'd love to test this out, however, I'm getting some errors. In the browser:
When I do a
|
Looking at this again, going to sort out the conflicts and try to get it working again. |
d412ad3
to
0154051
Compare
I have rebased this and started to address the review points. Most of the smaller ones have been addressed, and it's working again. I'll carry on with the points from #7635 (comment) and explain further what I mean about the |
@aduth - from #7635 (review)
Yeah, this is a kind of hand-wavey don't look behind the curtain answer but maybe controls that can be applied to multiple blocks need to have the attribute naming conventions I describe above, for this first iteration. Do we have any controls in core that have these non-standard type names, could apply to multiple selected blocks, and wouldn't work with this approach? If so, ignore this and replace it with "yes, we need to let the control provide some kind of mapping". Using the render prop approach would make this easier, but I think there it's a trade-off between implementing the controls more easily and implementing the block more easily. I'm more inclined to try to make block development as easy as possible, because we have far more new JS developers coming over from PHPland wanting to implement blocks, than new developers wanting to implement formatting controls and the like.
The naming in this PR is wrong I think. Designating a
I said I'd explain what I meant here :) So we have the |
Not sure on the status of this one, I'm not working on WordPress now, so I'll leave this open in case anyone else wants to continue, or happy to close it if other efforts are ongoing for the functionality. |
I am wondering if this PR is somewhat similar to what Pascal @swissspidy is working on these days. |
Would love to follow this as well. |
Trying to triage PRs today. Given that we're not investing in this right now and that the PR needs to be rewritten entirely with all the changes in master. I'm going to close this PR for now. We can reopen if there's a change in priorities. Thanks all for your efforts. |
Description
Similar PRs: #1811, #3535.
This adds a new controls option in a block's settings so the toolbar can be reused when multiple blocks of the same type are selected, and have theEdit
component render them automatically for individual blocks.Adds multi block controls and higher order components to deal with multiple block selections, so block developers can define their multi block aware controls in the edit component, and have them used to both single and multiple blocks.
Why a new option in the settings?
I wanted to have an easy way for block developers to define a toolbar and use it in their block, and also have it used byblock-list/multi-controls
to render a toolbar that can affect multiple blocks. It's optional, so maintains backward compatibility with existing block settings.There isn't any more! It doesn't change the API.
Why is it just a standalone function?
So that the block can pass in its own props and state if the toolbar needs it, and the multi controls can create props with attributes reduced from the selected blocks.It's not! It allows you to define a set of multi block aware components in
edit
and have them reused for single block instances too.This approach should work for the Inspector controls.
Why does it look so bad?
Because. Help! 😄It doesn't now!
Works though, doesn't it?
Yep.