-
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: Extensibility: Define anchor behavior as filtered blocks support #3318
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3318 +/- ##
==========================================
+ Coverage 33.92% 34.07% +0.14%
==========================================
Files 254 257 +3
Lines 6729 6753 +24
Branches 1219 1226 +7
==========================================
+ Hits 2283 2301 +18
- Misses 3751 3754 +3
- Partials 695 698 +3
Continue to review full report at Codecov.
|
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 like how this is shaping up.
To your point
Overriding edit and save behavior is made challenging
Do you think it would be a good idea to provide "helpers" as decorators.
const newSettings = flow( [
addInspectorControl( props => control ),
addToolbarControl( props => control ),
addContainer( { children } => <div>{ children }</div> ),
extendSaveContainer( props => extraContainerProps )
] )( settings )
name, | ||
attributes: get( window._wpBlocksAttributes, name ), | ||
...settings, | ||
}; | ||
|
||
return block; | ||
settings = applyFilters( 'registerBlockType', settings, 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.
Should we revert the arguments to match the registerBlockType
function signature? I know it's not convenient for the anchor filter because the name is useless.
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 awkward, I agree, but the behavior of filtering is such that we need the second argument to be the original value we expect to be filtered; in this case, the settings. All additional arguments are considered "extra", and in this case the name could prove valuable and is not otherwise made available.
See: https://developer.wordpress.org/reference/functions/apply_filters/
blocks/hooks/anchor.js
Outdated
} | ||
|
||
// Extend attributes with anchor determined by ID on the first node. | ||
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.
Should we avoid to edit the function parameter, Should we make this pure instead?
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 sounds like something that would be difficult to enforce, and would have questionable value if not enforced consistently. If it could be enforced, I could potentially see some value in strict reference comparisons on before/after a filter is applied (similar to what we see in how Redux reducer values are used).
As implemented here, we get some performance gains from mutating the object directly rather than first creating a shallow clone. I think it's reasonable to consider the filtering as an extension of the internal behavior of registerBlockType
, in which we have no qualms to mutate the settings object directly.
I could go either way personally, but the immediate value isn't obvious to me.
docs/block-api.md
Outdated
|
||
Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link. | ||
- `supportAnchor` (default `false`): Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link. |
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 this be anchor
instead of supportAnchor
?
blocks/hooks/anchor.js
Outdated
}; | ||
|
||
return settings; | ||
} |
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.
What your thoughts on unit testing this, is it possible, simple enough?
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, I should plan to write some tests here.
Btw, I'm working on #2541 locally and the work being done here to simplify the parser and the serializer would really help when generalized to all the advanced attributes (custom className and also the generated classNamee). |
That's a neat idea to help abstract some of the more complex common behaviors! |
0a38901
to
6c22b41
Compare
I've rebased and taken a revised approach to try to avoid concerns around block implementers needing to handle all the conditions which can occur in extending a block (e.g. save might be a function or a component, and a function returning an element or a markup string). The idea is that by applying filters more granularly (i.e. at serialization time, in addition to registration time), a plugin extender can be confident that a filter would only be fired if all prerequisites are already met (e.g. the There's not too many downsides, but a few potential concerns around:
|
@@ -55,7 +56,7 @@ export function getSaveContent( blockType, attributes ) { | |||
return element; | |||
} | |||
|
|||
const extraProps = {}; | |||
const extraProps = applyFilters( 'getSaveContent.extraProps', {}, 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.
This one seems to be a very specific. Maybe it should be simply called addAdvancedAttributes
instead of getSaveContent.extraProps
to avoid dot notation in the 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.
Actually, I think getSaveContent.extraProps
convey the correct meaning. I don't care much about the dot notation
} | ||
|
||
export default function anchor( { addFilter } ) { | ||
addFilter( 'registerBlockType', 'core\anchor-attribute', addAttribute ); |
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 distinguish filter names somehow between block lifecycle methods like registerBlockType
or getSaveContent
and editor components like BlockEdit
? Maybe it's enough that a component starts with upper 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.
To this and the previous point, I agree we need some conventions (also noted in #3318 (comment)). The dot notation was an idea for "filter within a function, but not filtering the function's return value", and the upper-case for components.
const hooks = createHooks(); | ||
|
||
const { | ||
addAction, |
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.
At this moment we use only:
addFilter
removeAllFilters
(accessed directly from@wordpress/hooks
)applyFilters
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 would have liked to simplify this to some pattern for exporting all available keys from createHooks()
, but it's not supported by module exports.
https://stackoverflow.com/questions/29844074/es6-export-all-values-from-object
Would you suggest limiting this only to those that are currently used?
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, I would take a similar approach to what we do for wp.element
, where we cherry-pick only what is really essential to make things work. I'm in favor of starting with a very limited lits of exposed methods and extend it whenever we have a real-life use 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.
It's still not public API, so we can iterate later. Anyway, we should definitely revisit when working on documentation.
I prefer this solution over #1023. API here is much easier to use, although it might be less flexible when compared with the decorator approach. I think we should start with the simpler solution and find out how far we can get with it. I also figured out that If we decide to merge this PR together with #3321 (slot & fill ), the only piece left would be Redux state. I will test it in-depth tomorrow, but I can already say that it looks solid enough that I anticipate that it's going to be accepted. |
6c22b41
to
357aa7d
Compare
export function hasBlockSupport( nameOrType, feature, defaultSupports ) { | ||
const blockType = 'string' === typeof nameOrType ? | ||
getBlockType( nameOrType ) : | ||
nameOrType; |
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.
Minor: but I tend to prefer functions with an unchanged signature. My preference would be to always use type
and call getBlockType
on the caller side when needed.
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 also makes documenting easier when you enforce calling getBlockType
explicitly. We can always provide two methods to support those 2 different ways of calling:
export function hasBlockTypeSupport( type, feature, defaultSupports ) {}
export function hasBlockSupport( name, feature, defaultSupports ) {
return hasBlockTypeSupport( getBlockType( name ), feature, defaultSupports );
}
The only challenge in such case is to find proper names for methods :)
* @param {Object} props Props passed to BlockEdit | ||
* @return {Element} Filtered edit element | ||
*/ | ||
export function addInspectorControl( element, 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.
Should we expose generic versions of those? Maybe, better to wait before that move.
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.
Nice work, let's get this in
@youknowriad @aduth Last thing to confirm before I merge this PR. Are we fine with removing Copy button from anchor? BeforeAfterI also noticed that it's no longer under |
357aa7d
to
7f3a223
Compare
I will open a follow-up with a proposal how to put Great job @aduth! This approach should make maintaining extensions points for blocks much easier internally in Gutenberg 👏 |
@gziolo Yeah, losing the Copy Link button was a noted compromise of this approach. We will need to consider this as part of a general pattern for extensions accessing post state. |
I opened #3475 to move back HTML Anchor under |
Closes #2474
This pull request seeks to explore an extensibility pattern for block settings, initially targeting one of Gutenberg's own extension points: block anchors. Other candidates include
className
andsupportHTML
property, with the idea being that this could develop into a general pattern for plugin authors to extend block behaviors.Implementation notes:
These changes make use of
@wordpress/[email protected]
to add a new hooks registry to the blocks default export, available atwp.blocks.addFilter
. The behavior here of adding a hooks registry specific to blocks (vs. a global registry) is not settled, but enabled as part of changes introduced to the hooks module in WordPress/packages#40.With these changes, a
registerBlockType
filter is fired before any block completes registration. Any filters may inspect and mutate the settings before the block is registered. The anchor supports behavior uses this opportunity to:anchor
attribute<InspectorControls />
in the edit form for managing the anchor valueid
attribute at save timeAs implemented, this is not a faithful adaptation. The main advantage is that extended behavior is now better isolated, vs. a previous ad-hoc solution which touched block parsing, factory, serialization, block API, and editor UI. This should prove to be more maintainable and scalable, keeping generalized behavior unaware to the existence of extensions.
There are a few compromises or open questions:
save
andedit
properties)To-Do:
Testing instructions:
Verify that when inserting a Heading block, you have the option via Inspector controls to add an anchor. After adding an anchor, saving the post, and refreshing the page, you should note that the anchor value is preserved, and that the
id
attribute exists in the saved markup.