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: Convert all filters for components to behave like HOCs #3827

Merged
merged 8 commits into from
Dec 12, 2017

Conversation

gziolo
Copy link
Member

@gziolo gziolo commented Dec 6, 2017

Description

This PR tries to bring idea exercised when working on Collaborative Editing in #3741. It turned out that it would be a better idea to use Higher-Order Components approach for using filtering hook with the existing components. This seems like a more natural way of dealing with React component and is de facto a community standard. This PR also tries to address other issues mentioned in other places:

#3800 (comment):

Alternatively we change blocks hooks to operate on the global hooks instance, then simply encourage plugin authors to register their hooks prior to the wp-blocks handle being loaded.

Related: #3493 (comment)

It's going to be updated with this PR. wp-hooks is always registered before wp-blocks, so it should be possible. This change enforces plugin developer to always register a hook before the modified module is going to to be imported.

#3621 (comment)

Let's make sure to reverse these changes now that the / hooks namespacing has been published.

c9310eb addresses it.

How Has This Been Tested?

Manually, e2e and unit tests.

Types of changes

Refactoring. No visual changes. Everything should work as before.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows has proper inline documentation.

@gziolo gziolo added [Feature] Extensibility The ability to extend blocks or the editing experience [Type] Enhancement A suggestion for improvement. labels Dec 6, 2017
@gziolo gziolo self-assigned this Dec 6, 2017
}

export default BlockEdit;
export default withFilters( 'BlockEdit' )( BlockEdit );
Copy link
Contributor

Choose a reason for hiding this comment

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

since the filters are "global", we should think of namespacing them: core/blocks/BlockEdit. It's not allowed right now in the hooks package and it's not required for this PR I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can put Blocks.BlockEdit until hooks package gets updated.

@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { createHooks } from '@wordpress/hooks';
import * as hooks from '@wordpress/hooks';
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we drop this and just import it when it's used (on each of the hooks files)?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could. I need to play with unit tests, but given that they are isolated, we should be good.

@youknowriad
Copy link
Contributor

youknowriad commented Dec 6, 2017

Good work here, I like it but I think this doesn't address the filters ordering issue yet. Imagine a plugin using reusable components from the "blocks" module to add hooks, it still depends on the "blocks" module and will be executed after the "registerBlockType" calls.

I think we should move the "registerBlockType" calls to the createEditorInstance function.

@gziolo
Copy link
Member Author

gziolo commented Dec 6, 2017

Good work here, I like it but I think this doesn't address the filters ordering issue yet. Imagine a plugin using reusable components from the "blocks" module to add hooks, it still depends on the "blocks" module and will be executed after the "registerBlockType" calls.

Good point. I didn't take into account such use case. Let's address it separately then :)

@gziolo gziolo force-pushed the update/component-filters-as-hoc branch from 7e3d4d4 to 58f7748 Compare December 6, 2017 10:01
@codecov
Copy link

codecov bot commented Dec 6, 2017

Codecov Report

Merging #3827 into master will increase coverage by 0.38%.
The diff coverage is 75%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3827      +/-   ##
==========================================
+ Coverage    37.7%   38.08%   +0.38%     
==========================================
  Files         279      279              
  Lines        6749     7183     +434     
  Branches     1228     1348     +120     
==========================================
+ Hits         2545     2736     +191     
- Misses       3541     3718     +177     
- Partials      663      729      +66
Impacted Files Coverage Δ
blocks/api/registration.js 100% <ø> (ø) ⬆️
blocks/api/serializer.js 98% <ø> (ø) ⬆️
blocks/hooks/index.js 100% <ø> (ø) ⬆️
components/higher-order/with-filters/index.js 100% <100%> (ø) ⬆️
blocks/hooks/generated-class-name.js 100% <100%> (ø) ⬆️
blocks/hooks/matchers.js 42.85% <100%> (ø) ⬆️
element/index.js 100% <100%> (ø) ⬆️
blocks/block-edit/index.js 100% <100%> (ø) ⬆️
blocks/hooks/custom-class-name.js 75% <66.66%> (-9.62%) ⬇️
blocks/hooks/anchor.js 72.22% <66.66%> (-7.78%) ⬇️
... and 10 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 798645e...58f7748. Read the comment docs.

@tg-ephox
Copy link
Contributor

tg-ephox commented Dec 7, 2017

@gziolo is it a good idea to allow hooks to change the BlockEdit element arbitrarily?

How will a plug-in implementor know what element a hook will receive if each hook can return anything it chooses?

The existing use cases for hooks are either adding a prop to BlockEdit or filling a slot. Can we make these explicit instead?

@gziolo gziolo changed the title Hooks: Convert all filters for components to behave like HOCs Extensibility: Convert all filters for components to behave like HOCs Dec 8, 2017
@gziolo gziolo force-pushed the update/component-filters-as-hoc branch from 6944810 to 7bba1db Compare December 8, 2017 12:08
@gziolo gziolo force-pushed the update/component-filters-as-hoc branch from ce2bbae to a46d71a Compare December 8, 2017 12:34
@gziolo
Copy link
Member Author

gziolo commented Dec 8, 2017

@gziolo is it a good idea to allow hooks to change the BlockEdit element arbitrarily?

How will a plug-in implementor know what element a hook will receive if each hook can return anything it chooses?

The same issue applies to all other hooks. We don't check the return value of the individual filters. I don't know how it is handled on PHP side. If the return value of the filter is validated then we should bake in a similar logic here. I feel like this is a completely different issue than what I was concerned when I proposed the changes with this PR.

The existing use cases for hooks are either adding a prop to BlockEdit or filling a slot. Can we make these explicit instead?

We were operating inside the render method of the existing <BlockEdit /> component, which was a wrapper for the original edit component registered together with a given block. By using an array, we were able to technically prepend or append the existing edit component, but we couldn't modify it because it was passed as an element with props already applied. The other issue I noticed is that you couldn't apply new props from state to the BlockEdit component, you could only compute values based on the existing props. This PR solves all of that by enforcing to always return new component from the filter. As long as plugin author will follow that rule, everything will work as expected. On the technical level, it allows to wrap, prepend, append or replace the existing edit component. It can be done by defining new component using functional representation as used in this PR. If the component extends Component class from React, then I assume, you could also extend this class to produce a different component.

@gziolo
Copy link
Member Author

gziolo commented Dec 8, 2017

I rebased with the latest changes. Added some small refinements. I'm planning to merge it on Monday morning if there are no blockers discovered.

addFilter( 'BlockEdit', 'core-custom-class-name-inspector-control', addInspectorControl );
addFilter( 'getSaveContent.extraProps', 'core-custom-class-name-save-props', addSaveProps );
export default function customClassName() {
addFilter( 'blocks.registerBlockType', 'core/custom-class-name/attribute', addAttribute );
Copy link
Member

Choose a reason for hiding this comment

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

I don't have strong opposition, but noting that there's a few implied conventions being introduced here:

  • Scoping filter names by with dot-separated namespaces
    • Any risk of confusion with other namespacing with /?
  • Multiple levels of namespacing, i.e. not just core/ but also core/custom-class-name/

Copy link
Member Author

Choose a reason for hiding this comment

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

Slash is not allowed for hook names... we need another patch if we want to change that.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can revisit namespaces, too. I’m fine with having only 2 first parts or open to other ideas.

customClassName( hooks );
generatedClassName( hooks );
matchers( hooks );
anchor();
Copy link
Member

Choose a reason for hiding this comment

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

Minor: Do we need to export these as functions anymore, or can they just call to addFilter when imported?

Copy link
Member Author

Choose a reason for hiding this comment

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

It might not play well with unit tests. Need to verify.

Copy link
Member Author

Choose a reason for hiding this comment

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

In theory, we could export individual functions and apply filters when importing. I will open a follow-up so we could discuss it individually.

@@ -534,4 +533,5 @@ export default compose(
isLocked: !! templateLock,
};
} ),
withFilters( 'editor.BlockListBlock' ),
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain the reason for this reordering?

Copy link
Member Author

Choose a reason for hiding this comment

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

I need to double check with Collaborative Editing. If it gets executed after connect, we can reuse props passed when filtering. Otherwise the get passed too late so we need to grab them using selectors twice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I can confirm it is correct. If withFilters would be first in the compose chain it would become the outermost component, which means React would render it first. As explained above, we want to have it last in the chain to make sure we have all other HOCs applied so we can take advantage of their modifications. This is how it makes the most sense for Collaborative Editing. Let's see how it works in general. We can refine later.

@aduth
Copy link
Member

aduth commented Dec 11, 2017

How will a plug-in implementor know what element a hook will receive if each hook can return anything it chooses?

Should a plugin implementer need to know whether it is the original BlockEdit or an enhanced one?

@gziolo
Copy link
Member Author

gziolo commented Dec 12, 2017

Let's merge it and focus on data extensibility :) We have just released 1.9.0 so that buys some time to iterate on extensibility.

@gziolo gziolo merged commit 050ea62 into master Dec 12, 2017
@gziolo gziolo deleted the update/component-filters-as-hoc branch December 12, 2017 06:02
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 [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants