-
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: Enable hook-based Slot/Fill registration #3321
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3321 +/- ##
==========================================
- Coverage 34.16% 34.12% -0.05%
==========================================
Files 258 261 +3
Lines 6755 6779 +24
Branches 1224 1225 +1
==========================================
+ Hits 2308 2313 +5
- Misses 3754 3773 +19
Partials 693 693
Continue to review full report at Codecov.
|
I tried the following: const H1 = wp.element.createElement( 'h1', {}, 'Hello World!' );
_wpGutenbergEditor.doAction( 'registerFill', 'publish-dropdown', () => H1 ); it worked like a charm: I like the proposed solution. It's very clean and elegant. It would be a great start to make Gutenberg extendable. It has some limitations at the moment:
Is there plan to support more advanced use cases in this PR? |
I really like this solution it is simple and works! I think it would be good to return some kind of identifier to the registerFill caller. Or receive a unique identifier from the caller. Using this identifier the artifact registering the component should be able to query if the component is registered or not (we would provide a mechanism to check), and remove or replace the component registered. I also think we should provide a way to check if the hook we want to register is available for us at the time or not. E.g there may be situations where we don't have a publish dropdown. |
editor/index.js
Outdated
@@ -68,5 +68,6 @@ export function createEditorInstance( id, post, settings ) { | |||
initializeMetaBoxes( metaBoxes ) { | |||
provider.store.dispatch( initializeMetaBoxState( metaBoxes ) ); | |||
}, | |||
...provider.hooks, |
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.
Do you think we could avoid having to register the hooks to the editor's instance. What if we have multiple instances?. I'm thinking a global like wp.postEditor.hooks
could be 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.
Do you think we could avoid having to register the hooks to the editor's instance. What if we have multiple instances?. I'm thinking a global like
wp.postEditor.hooks
could be better.
Right, this is a bigger open question about hooks generally. It seems in this case we have a few options:
- A single hooks registry, e.g.
wp.hooks.doAction
- A hooks registry common to all editors, e.g.
wp.postEditor.doAction
- A hooks registry specific to a single instance of an editor, e.g.
myEditor.doAction
On the last two, it depends if we think it would be common for multiple instances of an editor to exist in the same screen and, if so, whether it's sensible that each have distinct or share common extension behaviors. And generally whether we think the scoping adds value (clarity? predictable flow?)
It's not too dissimilar to as in Redux, we could have a module exporting a global shared instance of the store (import store from '../store';
) rather than passing it around as an argument when initialized (usually through the React Redux Provider context).
editor/plugin-slots/index.js
Outdated
} | ||
|
||
componentDidMount() { | ||
this.props.hooks.addAction( 'registerFill', 'plugin-slots-register', this.registerFill ); |
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.
Do we really need to update the fills after the initial render? I'm thinking the hooks could be global and just provide them in the editorSettings object. Could simplify things and avoid another provider.
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.
Do we really need to update the fills after the initial render? I'm thinking the hooks could be global and just provide them in the editorSettings object. Could simplify things and avoid another provider.
This could work, and is similar to the second "alternative" option described in the original comment (with some downsides mentioned).
These limitations are to be expected as the behavior of Slot / Fill to be a mechanism for "insert thing at this specific position". Arguably it's even desirable, if per #1352 we want to be conscious of specifically where we enable extension points. Beyond what exists currently, it would be fairly easy to create multiple slot areas: I could imagine "Before" and "After" pairings, so it's not merely appending. You may also be interested in the early exploration at #1023 which was more along the lines of extending or replacing the rendering behavior of a component. Another advantage that Slot / Fill has is that it's a much simpler concept for plugin authors to implement. |
Also some familiarity between Slots and a |
I have still a concern that we don't have control over which item gets appended first if multiple plugins compete to extend the same slot. I think this is why PHP hooks provide this
Oh, that's interesting. I will look into it tomorrow. This is exactly what I would need to implement for Collaborative Editing. There is this use case where you need to freeze block when another user is editing it. So it seems like the easiest way would be to wrap a component with a parent component that would render a different state when a block is being edited or simply render a default state otherwise. |
This priority argument exists in the JavaScript implementation as well: |
Nice 👍 In that case we only need to finalize how developers could consume this API. I think it's worth referencing here how it works for blocks at the moment: window.wp.blocks.registerBlockType( 'block-name', {
...
} ); I'm seeing the following options proposed:
So the question is if we want to hide the fact that we use hooks API behind the scenes and follow the pattern started with blocks, which would translate into something like: const H1 = wp.element.createElement( 'h1', {}, 'Hello World!' );
window.wp.editor.registerFill( 'publish-dropdown', () => H1 ); which would be close to the option (2) that updates all editor instances. Whatever we pick here, it seems like registering a new block type should follow the pattern. |
@gziolo Yes, there's a general overarching question as to whether we want to expose the raw actions/filters interface, or abstract them with specific APIs that may or may not use hooks as the implementation. I think it may come down to whether want to encourage a proliferation of actions and filters as we see in PHP, to the extent where creating specific APIs for each would be more confusing/redundant than helpful. A conservative approach would be to start with the specific APIs, avoid exposing the hooks, and reconsider later if we find that we are encountering said confusion. In the case of registering fills, since Slot/Fill gives us a good amount of flexibility, it should have a broad impact in allowing UI extensibility. #3330 can serve as a working overview of where we expect extensibility to be needed. |
Personally, I’m fine with exposing it as an experimental feature with API like |
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.
Let's get this shipped to try it in the Colloborative Editing Plugin.
I still have a preference for wp.postEditor.hooks.doAction
but I don't mind us trying the instance hook.
We should make sure to document this as experimental, with Docs or my renaming this to myEditor.experimentalHooks.doAction
Rebased to account for folder restructuring of #3389. Small refactor in a448cb7 to no longer expose the full hooks registry, but rather only currently a singular slot registry API, e.g. |
Extensible via slot hooks
Not yet considering to expose internal hooks registry yet, to see whether fill registration is sufficient
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 looks really good as a pattern and as a first step to extensibility.
I favor a conservative roll-out of the API, so the recent addition of the editor.registerFill
gatekeeper is 👍 .
I also think we should provide a way to check if the hook we want to register is available for us at the time or not. E.g there may be situations where we don't have a publish dropdown.
I think we can explore this as we go, along with starting to expose app state to 3rd-party consumers and seeing where that takes us.
constructor() { | ||
super( ...arguments ); | ||
|
||
this.hooks = createHooks(); |
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.
Was it tested with the recently added recreate editor instance feature? createHooks()
makes me think that there is going to be a new object created with a fresh state, so we might lose all registered hooks.
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.
2nd thing we should figure out pretty soon is how it relates to createHooks()
call for blocks: https://github.com/WordPress/gutenberg/blob/master/blocks/hooks/index.js#L11.
Unless createHooks
uses singletone pattern and it isn't an issue 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 don't think it does @gziolo - did you ever look into this?
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 doesn't use singletone . It's fine as it is, we took a completely different approach anyways :)
Okey, I can confirm that whenever an error is thrown inside the editor, it is not possible to recover using the newly introduced _wpGutenbergEditor.registerFill( 'publish-dropdown', () => { throw new Error() } ); It's something that doesn't make too much sense but allows to test it with easy Once you execute this code snippet on your JS console, you need to click Publish button in the top toolbar. You will see
|
I'm not opposed to merging it as it is, but wanted to make sure we keep track of it. |
I think we discovered other ways to achieve a similar goal. I'm closing this for now. We can also cycle back if we find out that the |
Related: #1352
This pull request seeks to explore an pattern for plugin UI extensibility by exposing slots on a global hooks registry.
Implementation notes:
These changes make use of
@wordpress/hooks@0.1.0-beta.5
to add a new hooks registry to the editor returned instance, available atwp._wpGutenbergEditor.addFilter
. The behavior here of adding a hooks registry specific to editor (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
registerSlot
action is monitored by a PluginSlots component after mounting. A plugin canfire this action toregister a fill component by slot name. For example, entering the following in the browser developer tools console while running this branch will cause a message to be shown in the publish menu dropdown:A few alternative approaches were considered:
editor.registerFill( name, FillComponent );
PluginSlots
here can instead leverage hooks context provided byHooksProvider
.PluginSlots
mount, i.e. instead ofthis.state = { fills: {} };
, doingthis.state = { fills: applyFilters( 'registerFills', {} ) };
The included
HooksProvider
component (andwithHooks
higher-order component) enable a hooks registry scoped to a specific React tree, rather than on a single global. Therefore, each editor instance returned bywp.editor.createEditorInstance
will have its own hooks registry (similar to having its own Redux store viaReactReduxProvider
).To-Do:
Testing instructions:
Enter the following into your browser developer tools console after loading the editor and verify that a "Hello!" message is shown when then toggling the Publish dropdown menu: