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

Extensibility: Define customClassname behavior as filtered blocks support #3472

Merged
merged 7 commits into from
Nov 17, 2017

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Nov 14, 2017

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:

supports: {
  customClassName: false
}

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 a extendElement helper. It can be improved IMO, open to suggestions on how to handle this better.

  • This also separates the generatedClassName from the customClassName to allow simple blocks like heading, paragraphs to support custom class names without having a generated one which was not possible. The generatedClassName 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

  • Try adding a custom classname on a heading block
  • Try an anchor on the heading block.
  • Save and refresh.
  • You should still see these values populated properly

@youknowriad youknowriad added the [Feature] Extensibility The ability to extend blocks or the editing experience label Nov 14, 2017
@youknowriad youknowriad self-assigned this Nov 14, 2017
@youknowriad youknowriad requested review from gziolo and aduth November 14, 2017 12:51
@youknowriad youknowriad force-pushed the update/custom-classname branch from 9f5dc00 to fb67afe Compare November 14, 2017 13:00
@gziolo
Copy link
Member

gziolo commented Nov 14, 2017

This removes the "advanced controls" panel, We could provide it back by grouping these two extensions into a unique one.

We can bring it back using the approach I propose in #3475 :)

@youknowriad youknowriad force-pushed the update/custom-classname branch from fb67afe to 7ce2647 Compare November 14, 2017 13:35
@codecov
Copy link

codecov bot commented Nov 14, 2017

Codecov Report

Merging #3472 into master will increase coverage by 0.03%.
The diff coverage is 90.9%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
blocks/hooks/anchor.js 80% <ø> (ø) ⬆️
blocks/library/more/index.js 22.22% <ø> (ø) ⬆️
editor/components/block-inspector/index.js 14.28% <ø> (ø) ⬆️
blocks/library/html/index.js 16.66% <ø> (ø) ⬆️
blocks/library/shortcode/index.js 40% <ø> (ø) ⬆️
blocks/api/factory.js 86.48% <ø> (-0.7%) ⬇️
blocks/api/parser.js 98% <ø> (-0.08%) ⬇️
blocks/api/serializer.js 98.18% <100%> (ø) ⬆️
components/slot-fill/fill.js 88% <100%> (+13%) ⬆️
components/slot-fill/slot.js 79.16% <100%> (+12.5%) ⬆️
... and 18 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update afd2627...5c38f6a. Read the comment docs.

};
const contentWithClassname = Children.map( saveContent, addAdvancedAttributes );
const contentWithClassname = Children.map( saveContent, addGeneratedClassname );
Copy link
Member

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 :)

Copy link
Member

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.

category: 'common',
title: 'block title',
attributes: {
className: {
Copy link
Member

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 );
Copy link
Member

@gziolo gziolo Nov 14, 2017

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 😃

*/
import { Children, cloneElement } from '@wordpress/element';

export function extendElement( element, newElement ) {
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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 ?

*/
export function addInspectorControl( element, props ) {
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) {
element = extendElement(
Copy link
Contributor Author

@youknowriad youknowriad Nov 14, 2017

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

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 :(

@@ -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=",
Copy link
Member

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 :(

Copy link
Member

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" />
Copy link
Member

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', () => {
Copy link
Member

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.

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, {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

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.

*/
export function addSaveProps( extraProps, blockType, attributes ) {
if ( hasBlockSupport( blockType, 'customClassName', true ) && attributes.className ) {
extraProps.className = classnames( extraProps.className, attributes.className );
Copy link
Member

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.

@gziolo
Copy link
Member

gziolo commented Nov 14, 2017

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 :)

@@ -0,0 +1,90 @@
/**
Copy link
Member

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.

if ( ! element || ! isObject( element ) ) {
return element;
}

const extraProps = applyFilters( 'getSaveContent.extraProps', {}, blockType, attributes );
const props = applyFilters( 'getSaveContent.extraProps', { ...element.props }, blockType, attributes );
Copy link
Member

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 😬

Copy link
Contributor Author

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.

Copy link
Member

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.

@@ -51,24 +51,23 @@ export function getSaveContent( blockType, attributes ) {
}

// Adding a generic classname
const addAdvancedAttributes = ( element ) => {
const addGeneratedClassname = ( element ) => {
Copy link
Member

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.

Copy link
Contributor Author

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

*/
import { Children, cloneElement } from '@wordpress/element';

export function extendElement( element, newElement ) {
Copy link
Member

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,
Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -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=",
Copy link
Member

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.

*/
import { Children, cloneElement } from '@wordpress/element';

export function extendElement( element, newElement ) {
Copy link
Member

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?

*/
import { Children, cloneElement } from '@wordpress/element';

export function extendElement( element, newElement ) {
Copy link
Member

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 ?

*/
export function addInspectorControl( element, props ) {
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) {
element = extendElement(
Copy link
Member

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.

@youknowriad youknowriad force-pushed the update/custom-classname branch from bcd6f60 to 6d94b55 Compare November 16, 2017 08:51
@gziolo
Copy link
Member

gziolo commented Nov 16, 2017

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:

class-additional

Without force prop it renders class name field as a first item in the sidebar when transforming it. The issue here is that React reuses existing fill and therefore it's always present on the list of children as the first item. We need to find a more elegant solution that will cover all similar use cases.

@aduth
Copy link
Member

aduth commented Nov 16, 2017

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.

@youknowriad
Copy link
Contributor Author

Team effort ❤️

const Edit = blockType.edit || blockType.save;
Edit.displayName = 'Edit';
let Edit;
if ( blockType ) {
Copy link
Member

@gziolo gziolo Nov 16, 2017

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 );
Copy link
Member

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" />,
],
} );
Copy link
Member

@gziolo gziolo Nov 16, 2017

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.

@@ -46,9 +45,9 @@ export function addAttribute( settings ) {
*/
export function addInspectorControl( element, props ) {
if ( hasBlockSupport( props.name, 'customClassName', true ) && props.focus ) {
element = concatChildren(
element = [
Copy link
Member

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.

Copy link
Member

@gziolo gziolo left a 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 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Extensibility The ability to extend blocks or the editing experience
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants