-
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
Extensibility: Define customClassname behavior as filtered blocks support #3472
Conversation
9f5dc00
to
fb67afe
Compare
We can bring it back using the approach I propose in #3475 :) |
fb67afe
to
7ce2647
Compare
Codecov Report
@@ Coverage Diff @@
## master #3472 +/- ##
==========================================
+ Coverage 34.36% 34.39% +0.03%
==========================================
Files 261 270 +9
Lines 6743 6885 +142
Branches 1229 1258 +29
==========================================
+ Hits 2317 2368 +51
- Misses 3734 3811 +77
- Partials 692 706 +14
Continue to review full report at Codecov.
|
blocks/api/serializer.js
Outdated
}; | ||
const contentWithClassname = Children.map( saveContent, addAdvancedAttributes ); | ||
const contentWithClassname = Children.map( saveContent, addGeneratedClassname ); |
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 real name should be sth like contentWithClassnameAndExtraProps
:)
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.
By the way, we probably should use ClassName
suffix everywhere. This classnames
utility makes it all confusing.
blocks/api/test/registration.js
Outdated
category: 'common', | ||
title: 'block title', | ||
attributes: { | ||
className: { |
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 wasn't aware it was possible, this is a nice way to ensuring that className
is defined as a string 👍
@@ -58,7 +58,7 @@ describe( 'anchor', () => { | |||
|
|||
it( 'should do nothing if the block settings do not define anchor support', () => { | |||
const attributes = { anchor: 'foo' }; | |||
const extraProps = addSaveProps( blockSettings, attributes ); | |||
const extraProps = addSaveProps( {}, blockSettings, attributes ); |
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.
Good catch, this is the issue with testing only for non-existence. It is very easy to satisfy 😃
blocks/hooks/utils.js
Outdated
*/ | ||
import { Children, cloneElement } from '@wordpress/element'; | ||
|
||
export function extendElement( element, newElement ) { |
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 nice and it seems very useful. We could add unit tests. I'm wondering if snapshot approach would fit here. I can play later with it, as we miss snapshot serializer for enzyme
at the moment.
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 wanted to support prepending to an existing element, could we make this more generic to extendElement( ...elements )
like an Object.assign
? Maybe closer to a concatChildren
behavior in that case.
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.
And to the above point, is this really a hook utility? Or something more appropriate for wp.element
?
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 fact... isn't this achieving the same behavior as concatChildren
?
blocks/hooks/custom-classname.js
Outdated
*/ | ||
export function addInspectorControl( element, props ) { | ||
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) { | ||
element = extendElement( |
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 noticed that extendElement
is breaking the cypress tests. You can just type "Enter" in a paragraph block to reproduce locally (JS error in the console). I suspect it's due to extra component "remounting" due to how we extend the base BlockEdit
. Does this mean, it's not a great idea to add "filters" to render functions? Not sure how to fix this, ideas @aduth
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 this as well. Also always focusing to the beginning of the text when clicking within. So far unsuccessful in debugging, but will continue investigating.
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 was able to fix this by avoiding updating the "keys". I was expecting the React "missing keys" warning but for some reason I'm not seeing it.
I'm not 100% confident here though. I would appreciate some help 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.
Actually, I was using the React's production build, so in "dev" I see the warnings. Problem is, adding the missing keys brings back the issue :(
package-lock.json
Outdated
@@ -1372,7 +1372,7 @@ | |||
"base64-js": { | |||
"version": "1.2.1", | |||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", | |||
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", | |||
"integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=", |
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 hash change thing is really annoying when working with npm5
:(
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 it is, and we should not be introducing these changes here. I can usually resolve the issue by resetting package-lock.json
to the original version, rm -rf node_modules
, then a fresh npm install
.
<Slot name="Inspector.Controls" /> | ||
<BlockInspectorAdvancedControls key={ `advanced-controls-${ selectedBlock.uid }` } /> | ||
</div> | ||
<Slot name="Inspector.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.
It conflicts with the changes I propose with #3475, but we can resolve that later if necessary.
*/ | ||
import customClassName from '../custom-classname'; | ||
|
||
describe( 'anchor', () => { |
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.
Copy & pasta anchor
-> custom className
.
blocks/hooks/custom-classname.js
Outdated
export function addAttribute( settings ) { | ||
if ( hasBlockSupport( settings, 'customClassName', true ) ) { | ||
// Use Lodash's assign to gracefully handle if attributes are undefined | ||
settings.attributes = assign( settings.attributes, { |
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.
@aduth is there any particular reason why the existing variable is mutated here? I think @youknowriad is following your implementation. It looks like a reason why let
needs to be used later in unit tests: https://github.com/WordPress/gutenberg/pull/3472/files#diff-1f4671e20828b625e6bfcb26ec4c7cdeR19. It would be nice to make it immutable, but maybe I'm missing something.
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.
Yes, thank. I discussed it with Andrew, too. It makes the most sense in the context of getSaveContent.extraProps
. Let's leave it as it is.
blocks/hooks/custom-classname.js
Outdated
*/ | ||
export function addSaveProps( extraProps, blockType, attributes ) { | ||
if ( hasBlockSupport( blockType, 'customClassName', true ) && attributes.className ) { | ||
extraProps.className = classnames( extraProps.className, attributes.className ); |
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 same as above. It would be nice to avoid mutation if possible.
This was super fast refactoring 🚀 It looks really solid. I left a few comments. Some of them should be really noted in the preceding PR, but I missed them. I don't think I discovered any blockers. I will give it a round of testing tomorrow unless @aduth accepts those changes earlier :) |
blocks/hooks/custom-classname.js
Outdated
@@ -0,0 +1,90 @@ | |||
/** |
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.
Filename should be custom-class-name.js
to reflect upper-case usage.
blocks/api/serializer.js
Outdated
if ( ! element || ! isObject( element ) ) { | ||
return element; | ||
} | ||
|
||
const extraProps = applyFilters( 'getSaveContent.extraProps', {}, blockType, attributes ); | ||
const props = applyFilters( 'getSaveContent.extraProps', { ...element.props }, blockType, attributes ); |
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.
Should we rename the filter to reflect that this aren't "extra" props, they're the source props themselves?
Raises a worry about changing filter names since there could be dependants 😬
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.
You could argue that you receive the props and add extra props and the name is still valid. Happy to change if you think it's better.
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.
You could argue that you receive the props and add extra props and the name is still valid. Happy to change if you think it's better.
Well, this could apply to most any filter, and I think the general tendency in PHP has been to name them without prefixes and more toward describing the source data. Also, the filters might remove options.
blocks/api/serializer.js
Outdated
@@ -51,24 +51,23 @@ export function getSaveContent( blockType, attributes ) { | |||
} | |||
|
|||
// Adding a generic classname | |||
const addAdvancedAttributes = ( element ) => { | |||
const addGeneratedClassname = ( element ) => { |
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 think serializer should have no knowledge of class name after these changes, given that className behavior is considered to be an extension of a core behavior where we simply allow hooks to override props. Really it seems the only thing we still rely on here is to call classnames
, but this could be moved into the block hook logic itself.
Edit: Nevermind, I see from the original comment that we are still accounting for the default class name. However, where do we use this feature? Can we drop it? I don't think we have use for it. Why couldn't a block author just assign className
in save
than by a separate block type property? Fine to do separately.
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.
However, where do we use this feature? Can we drop it?
Yes, we do use it, that's how the generated className for all blocks is added.
Why couldn't a block author just assign className in save than by a separate block type property?
I was also thinking it could be a separate extension. But yeah, let's fix this separately
blocks/hooks/utils.js
Outdated
*/ | ||
import { Children, cloneElement } from '@wordpress/element'; | ||
|
||
export function extendElement( element, newElement ) { |
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 wanted to support prepending to an existing element, could we make this more generic to extendElement( ...elements )
like an Object.assign
? Maybe closer to a concatChildren
behavior in that case.
@@ -33,6 +33,10 @@ registerBlockType( 'core/html', { | |||
|
|||
supportHTML: false, | |||
|
|||
supports: { | |||
customClassName: false, |
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.
Can we now drop the className: false
from this and other revised blocks?
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 can't, it controls the generated className which we should address separately.
package-lock.json
Outdated
@@ -1372,7 +1372,7 @@ | |||
"base64-js": { | |||
"version": "1.2.1", | |||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", | |||
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", | |||
"integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=", |
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 it is, and we should not be introducing these changes here. I can usually resolve the issue by resetting package-lock.json
to the original version, rm -rf node_modules
, then a fresh npm install
.
blocks/hooks/utils.js
Outdated
*/ | ||
import { Children, cloneElement } from '@wordpress/element'; | ||
|
||
export function extendElement( element, newElement ) { |
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.
And to the above point, is this really a hook utility? Or something more appropriate for wp.element
?
blocks/hooks/utils.js
Outdated
*/ | ||
import { Children, cloneElement } from '@wordpress/element'; | ||
|
||
export function extendElement( element, newElement ) { |
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 fact... isn't this achieving the same behavior as concatChildren
?
blocks/hooks/custom-classname.js
Outdated
*/ | ||
export function addInspectorControl( element, props ) { | ||
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) { | ||
element = extendElement( |
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 this as well. Also always focusing to the beginning of the text when clicking within. So far unsuccessful in debugging, but will continue investigating.
bcd6f60
to
6d94b55
Compare
I added 3d54d33 which is a dirty hack to make rerender of the sidebar toolbar to be predictable when transforming block between different types. See the following screencast to learn about the issue: Without |
Pushed 73c3e30 as an attempt to resolve the ordering issue. Essentially, we were previously unable to rely on the order of fills because the behavior of registration was simply appending. In the above example, when switching block type, the base inspector controls for the original block type unmount, but the class name field does not (because it is common to both). So when the next block type inspector controls render, they are appended after the class name field. With 73c3e30, we add order tracking to fills, both when it initially mounts, and after any other fill within that slot unmounts, assuming that updates will occur in the expected render order. Then the slot renders using fills ordered by this occurrence. |
Team effort ❤️ |
blocks/block-edit/index.js
Outdated
const Edit = blockType.edit || blockType.save; | ||
Edit.displayName = 'Edit'; | ||
let Edit; | ||
if ( blockType ) { |
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 changed that because there is the opposite check above:
if ( ! blockType ) {
return null;
}
This check is obsolete. It will never be falsy here.
@@ -61,6 +61,7 @@ class SlotFillProvider extends Component { | |||
this.fills[ name ], | |||
instance | |||
); | |||
this.resetFillOccurrence( name ); |
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 think this is the most important line. It causes the key to update in slot
implementation. 💯
<Filler name="egg" key="first" text="first" />, | ||
<Filler name="egg" key="second" text="second" />, | ||
], | ||
} ); |
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.
element.setProps( {
children: [
<Slot name="egg" key="slot" />,
<Filler name="egg" key="second" text="second" />,
<Filler name="egg" key="first" text="first" />,
],
} )
I tried the following instead of line 122 and it outputs <div>secondfirst</div>
. Which means you can use still use concatChildren
, because it doesn't care about the keys passed. However it is predictable because it resets occurences for every slot whenever any fill gets removed.
If I add the same code after line 128, it still renders <div>firstsecond</div>
because occurrences are not modified.
blocks/hooks/custom-class-name.js
Outdated
@@ -46,9 +45,9 @@ export function addAttribute( settings ) { | |||
*/ | |||
export function addInspectorControl( element, props ) { | |||
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) { | |||
element = concatChildren( | |||
element = [ |
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 tested it a bit, it works with concatChildren
, too.
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.
All issues were resolved. Great team work! 🚀
I'll give it another round of testing and merge 🎉
This follows the work done in #3318 and uses the same extensibility APIs to define the custom className behavior.
Unlike the anchor, this customClassname is opt-out. to opt-out of if, you need to add the following code to the block settings:
This surfaces some issues with the anchor definition:
This update the
extraProps
filter by passing the original element props to the first filter. This is necessary because we have to append the custom className to the existing className prop.we can't call
cloneElement
on element fragments which break the anchor/classname filters if you use both on a single block. To resolve this I introduced aextendElement
helper. It can be improved IMO, open to suggestions on how to handle this better.This also separates the
generatedClassName
from thecustomClassName
to allow simple blocks like heading, paragraphs to support custom class names without having a generated one which was not possible. ThegeneratedClassName
bahavior could also be refactored to use the extensibility API.This removes the "advanced controls" panel, We could provide it back by grouping these two extensions into a unique one.
Testing instructions