Declare and override autocompleters via filter#4609
Conversation
|
If others agree this approach is reasonable, I'd also like to add a filter for transforming the default list of completers. For my use case, I'd want to add a completer for cross-posting to different P2s (e.g., |
91b616e to
72d8371
Compare
components/autocomplete/index.js
Outdated
There was a problem hiding this comment.
We need to standardize the way we name filters to have one pattern that is easy to follow. So far we were prefixing the hooks name with the package name followed by the component's name. So, in this case it would be something like: components.Autocomplete. Another thing here is that we can combine those 3 filters into one as follows and call it once only when the component gets initialized:
this.props.completers = applyFilters( `components.Autocomplete.completers', this.props.defaultCompleters );|
@brandonpayton, you can totally go for it. It's a valid use case and we should support a wide range of extensibility options. My only concern that your initial proposal it too granular and it would make it harder to discover for plugin developers. Another concern I have is that ideally all hooks should be applied only once when a component gets mounted. If we want to allow to dynamically add/remove/update hooks we can do it using event listeners provided by the |
|
Thanks for the review, encouragement, and guidance, @gziolo! I think you're right about the granularity, especially for a first pass. I'll update with a single filter that runs after the component is mounted. |
|
I noticed that @noisysocks is working on some refactoring related to autocompleters in #4769. Cross-linking for better visibility :) |
|
@noisysocks and I spoke briefly yesterday. Autocompleting reusable blocks means the completer needs access to the latest list. I spent some time thinking today, and here's what I'm seeing.
Here's an example of what I'm imagining for the blocks completer. {
completes: 'blocks',
className: 'blocks-autocompleters__block',
triggerPrefix: '/',
getOptions,
getOptionKeywords,
renderOption,
allowContext,
getOptionValue,
onSelect,
}I'm interested in your thoughts. If you have time, let's talk more on Monday. |
I may be wrong about this if the only expected context for a blocks completer is the editor. I've been assuming the editor only applies when editing a post, but if it also will be used in things like customization, then it probably doesn't make sense to provide a default I think the main thing for this PR is that option retrieval should be separated from rendering, so each can be overridden separately. |
|
@brandonpayton, I'm looking forward to seeing this PR updated. I need to go through the code to better understand how this would work. I don't know that well the internal implementation of autocompleters :) |
|
@gziolo I'm eager to work on this today but have to take care of something first. It might have to wait until tomorrow. Thanks for your comments and interest! |
72d8371 to
3b17dec
Compare
|
I updated the PR to...
I am working on...
|
|
I ran into serious a11y questions with moving to a single top-level Autocomplete instance and am keeping multiple instances for now. I updated Autocomplete to look up completers based on its containing block type but still need to update the tests. I also changed completers to simply return a result for a selected option and made it the responsibility of Autocomplete to insert text or replace the current block. This is the general idea, but it probably needs some cleanup. I'd still like a way for autocompletion to automatically apply for blocks with defined autocompleters, but I'm not sure how to do that in an accessible way since autocomplete-related aria attributes and UI should be directly associated with each input field. |
|
Currently, I'm thinking autocompleters should be added to the context with |
I'm thinking Autocomplete should remain a simple props-driven This will allow autocompleters to be declared along with block registration and overridden at that granularity. To support blocks with multiple text inputs and different autocompleters for each, this will be insufficient. I'm thinking we should get this working at the block level first and iterate if we need further granularity. |
blocks/autocompleters/index.js
Outdated
There was a problem hiding this comment.
If you want to make sure this is called only once you can use lodash.once
const getOptions = once( () => Promise.resolve... );There was a problem hiding this comment.
Thanks for the tip! That's cleaner.
I'll make the update.
blocks/autocompleters/index.js
Outdated
There was a problem hiding this comment.
I don't know this code so just wanted to double check why to remove onReplace here.
There was a problem hiding this comment.
It started with a dependency issue when moving completers to block registration. We didn't yet have an onReplace.
Then I thought completers should be more declarative anyway. The presence of an onSomething name made me think about that. Why should a completer know that it is replacing something in the editor? The idea is that the editor has a better idea of what it wants to do with the result of a completer selection.
There was a problem hiding this comment.
I think it ensures we remove the existing paragraph block.
There was a problem hiding this comment.
The Autocomplete component now calls onReplace instead when a block is returned.
ae62091#diff-8d7b3f0b32214bb1340578e3be07ed96R176
The idea is that a completer should not be involved in making actual changes to the content.
blocks/autocompleters/index.js
Outdated
There was a problem hiding this comment.
I like this part, it makes it nicely composable out of smaller pieces.
components/autocomplete/index.js
Outdated
There was a problem hiding this comment.
Is there maybe an existing API method which validated if the input is a block? There is a similar logic in here:
https://github.com/WordPress/gutenberg/blob/master/blocks/block-edit/index.js#L22
We might want to extract this code and put in the method that would be used it in both places.
There was a problem hiding this comment.
Agreed. I'll look at that.
There was a problem hiding this comment.
I took a look and am wondering whether it is worth it to a function for the check. The only thing in common is that both make sure a truthy value is returned from getBlockType. Defining an isBlockType function in blocks/api/registration may allow us to be more direct though.
Currently, I simply moved the conditions from components/Autocomplete to blocks/contextual-autocomplete in the form of the isReplacement prop.
blocks/autocompleters/test/index.js
Outdated
There was a problem hiding this comment.
It looks like you can perform all assertions inside this loop.
blocks/autocompleters/test/index.js
Outdated
There was a problem hiding this comment.
Or can we make it more explicit what is what? At the moment it might work, but it's hard to decipher what we are checking here.
blocks/library/paragraph/index.js
Outdated
There was a problem hiding this comment.
We might want to iterate on getBlockType passed as prop. Maybe it should be a part of the autocompleter object. Not sure.
There was a problem hiding this comment.
This will go away when I make the update for BlockEdit to provide autocompleters via context. There will be no need to find the containing block type to provide autocompletion context because that will be provided naturally via React context.
blocks/library/paragraph/index.js
Outdated
There was a problem hiding this comment.
It probably makes the most sense to have it here 👍 Let's see how it goes :)
There was a problem hiding this comment.
Is there a particular use case we have in mind for having a block type define what autocompleters it wants? (I like to err on the side of you aren't gonna need it, especially since registerBlockType is a public API.)
There was a problem hiding this comment.
You might want different autocompleters per block. A concrete example is that a block autocompleter makes sense in a paragraph but not in the caption field of a cover image. If you offer block completion there, it is more likely an opportunity for surprising replacement than a feature IMO.
There was a problem hiding this comment.
What If you want different autocompleters per RichText? is this a valid use case? could this be a prop of RichText instead?
There was a problem hiding this comment.
I'm not sure, but it seems reasonable to want different autocompleters per RichText. I'm not sure what overriding should look like in that case though.
I'm wondering whether this PR is trying to do too much and, in retrospect, would like to have created an autocompletion extensibility issue for discussion and smaller PRs. But if we can adjust in the future, maybe it would be good to continue as-is and get more feedback once merged.
components/autocomplete/index.js
Outdated
There was a problem hiding this comment.
Yes, we might want to inject this, too. This is really interesting :)
|
I like where it is heading. It makes autocompleters more general. I'm wondering if we could use render-props pattern. If you don't know the concept I recommend this article: https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce or downshift library: https://github.com/paypal/downshift. We used it also with the |
ed0ea5d to
f0f3c5b
Compare
bf0c6ce to
e3277e1
Compare
|
I updated this PR according to review notes made by @pento and believe it is ready for another review.
cc @gziolo |
|
The API for creating autocompleters is feeling a lot more accessible, nice work @brandonpayton! Could you add a Other than that, 👍🏻 from me for the API. @gziolo, could you review the internals? |
gziolo
left a comment
There was a problem hiding this comment.
I started the review. I left a few comments. I will download all code tomorrow and perform some testing.
There was a problem hiding this comment.
There are 3 additional divs in here, at least 2 of them look obsolete. It should be investigated further. Hopefully, it can be fixed.
There was a problem hiding this comment.
I noticed this as well and was initially puzzled, but they are divs added by the Autocomplete component now used within RichText:
Autocompleteadds a div to wrap TinyMCE, adding event listeners and a sibling popup.withFocusOutsidewrapsAutocompleteand adds a parent div because it adds event listeners.- The autocompleters hook adds another parent div in order to add its own event listeners.
I dislike this but do not currently see a better solution. I would love to be able to add event listeners to a Fragment (mentioned here as a possible future capability) to avoid rendering the elements. I'll give it some thought. Do you have any suggestions?
docs/extensibility.md
Outdated
There was a problem hiding this comment.
completers is not used inside the function, should be removed.
docs/extensibility.md
Outdated
There was a problem hiding this comment.
completers is not used inside the function, should be removed.
blocks/hooks/autocompleters.js
Outdated
There was a problem hiding this comment.
It can be also simplified to:
updateFilteredCompleters( nextCompleters = defaultCompleters ) {by using a default param value.
There was a problem hiding this comment.
Thanks, I'm not sure how I missed that :). Based on one of your other comments, I've updated the PR to provide defaults via a filter instead, so this no longer applies.
blocks/hooks/autocompleters.js
Outdated
There was a problem hiding this comment.
There is also hasFilter method which would be useful here to avoid state updates when there are no filters registered.
There was a problem hiding this comment.
In this case, the state must always be updated after new completer props are received because the filtered completers are maintained in the state. This is done on demand when the component first contains focus after receiving new completer props.
We can still use hasFilter to avoid copying the completers list when there is no filter added, and I'm making that change now.
blocks/hooks/autocompleters.js
Outdated
There was a problem hiding this comment.
There is a convention to prefix all HOCs with with prefix. In this case, it might be better to name it withFilteredAutocompleters.
blocks/hooks/test/autocompleters.js
Outdated
There was a problem hiding this comment.
It'd be less code to put wrapper.unmount() inside afterEach call instead of using try / finally in all tests.
There was a problem hiding this comment.
I was actually doing it that way initially but switched away because I thought I would be able to use shallow for most of the tests. I was wrong. :)
I started to prefer the explicit try/finally because it is easy to accidentally declare a local const wrapper that shadows the outer variable, but I just noticed we have a no-shadow lint rule that covers this case.
I'll switch it back.
blocks/hooks/autocompleters.js
Outdated
There was a problem hiding this comment.
As far as I understand what happens here, it seems like it would be enough to have only one filter registered for autocompleters: components.Autocomplete.completers.
Most of the logic that is inside this file is directly related to the Autocomplete component. Instead of adding filter on the component.Autocomplete, it is also possible to set default autocompleters using:
const setDefaultAutocompleters = () => [ ...defaultCompleters ];
addFilter(
'components.Autocomplete.completers',
'core/autocompleters/set-default-autocompleters',
setDefaultAutocompleters
);There was a problem hiding this comment.
I'm not sure I correctly understand your comment, but my reading is that this component wrapper may not be necessary. Is that correct?
The purpose of this module is to filter autocompleters passed to Autocomplete components and to provide default completers when none are provided. The filtering is done on-demand per Autocomplete component when focus first enters the component after receiving new completers via props.
Your comment made me realize it would be more idiomatic to provide default completers via a components.Autocomplete.completers filter, and I've made that update.
|
I added the first version of a README to |
1661d2a to
5c637d0
Compare
gziolo
left a comment
There was a problem hiding this comment.
I left a few more comments, which circle back to my latest comments. In addition, I would like to make sure we have a good strategy for rollout. We should do our best to inform plugin developers about breaking changes introduced.
docs/extensibility.md
Outdated
There was a problem hiding this comment.
There were some major updates to the extensibility docs. I think this should now land into its own file.
There was a problem hiding this comment.
OK, I will move them in an autocompletion.md doc. Does that work for you?
There was a problem hiding this comment.
It all depends if you decide to have it as block specific or component specific :)
Here is the document for Extending Blocks.
It might be also its own Extending Components document. Autocompletion might be too granular.
components/autocomplete/index.js
Outdated
There was a problem hiding this comment.
wp.components.Autocomplete was exposed for some time. Is there any way to make it backward compatible for the next 2 release cycles? We should also add a deprecation message to give a warning about the changes.
There was a problem hiding this comment.
We should be able to detect a difference in the completer objects and log a deprecation message then. Do we want to limit such messages or print for every completer object seen with the old interface?
There was a problem hiding this comment.
In other cases, it was done per function/component occurrence. See #5833 for reference.
blocks/autocompleters/README.md
Outdated
There was a problem hiding this comment.
I have a feeling that this should be documented next to the original Autocomplete component. It is also breaking change as far as I understand. We should provide some notes how to update code when someone was using wp.components.Autocomplete.
There was a problem hiding this comment.
I tend to agree with you. I don't know why the completer interface should be documented within @wordpress/blocks and not with the wp.components.Autocomplete component. I plan to move the completer interface JSDoc into components/autocomplete/index.js. Sound reasonable?
The blocks completer probably belongs within @wordpress/blocks.
I feel better keeping the user completer within @wordpress/blocks as well because @wordpress/components seems to be more generic than the users module which actually hits the WP REST API.
There was a problem hiding this comment.
We should provide some notes how to update code when someone was using wp.components.Autocomplete.
Where is a good place to provide such notes?
There was a problem hiding this comment.
I plan to move the completer interface JSDoc into components/autocomplete/index.js. Sound reasonable?
Yes, this was my exactly my point. It's a general interface that should work with every custom Autocomplete component.
Where is a good place to provide such notes?
I think it is enough to include deprecated function in the code and leave a link to the new interface since it is now very well documented.
See also: https://github.com/WordPress/gutenberg/pull/5398/files#diff-cf74d2aaa31578636c008cace4de69f2L13.
blocks/autocompleters/index.js
Outdated
There was a problem hiding this comment.
I'm not convinced that we should have the default setup exposed in here. We should rather set it in the hook.
There was a problem hiding this comment.
Agreed. I think that is a better idea. I'll make a note to make the update once we decide what to do instead of the components.Autocomplete filter.
blocks/hooks/autocompleters.js
Outdated
There was a problem hiding this comment.
I still don't understand why we need components.Autocomplete filter. It is applied globally so it wraps all autocompleters. Could it be applied directly in components/Autocomplete? Or should we alternatively create BlockAutocomplete component and use it inside RichText?
There was a problem hiding this comment.
I don't believe there is anything here that requires the components.Autocomplete filter. I am trying to remember my frame of mind when I wrote this. In this comment, I may have thought you were suggesting we add this functionality separately via such a filter, so it is added when blocks are in use. Now that you mention it though, I suppose block and non-block UI may exist simultaneously so that we don't want to do this for all instances.
Should autocompletion extensibility be a feature limited to @wordpress/blocks or also supported for @wordpress/components?
My mind has been set on blocks, and I have not given much thought to WordPress UI outside of blocks. I don't yet know what I think about that question, but an answer will help us know what to do instead of this unnecessary filter.
If it is a blocks-only feature, I think creating a BlockAutocomplete component makes sense to provide the on-focus behavior currently added via the components.Autocomplete filter, and we should also use a blocks-related filter name instead of the generic components.Autocomplete.completers.
I'm thinking that the opinion of a default completers list should also be limited to blocks, but if we want autocompleters to be extensible for @wordpress/components as well, I am not sure how to filter and provide defaults only within a block context because the filter could be applied in both block and non-block contexts.
There was a problem hiding this comment.
I went through a similar thinking process for MediaUpload component in #5846. I wanted to move it to components module, but the feedback I received suggests to leave it inside blocks module together with the accompanying filer. I think it makes a lot of sense because the idea is to have a set of independent components as @youknowriad explained:
I'm not sure we want to move components relying on filters to the components folder. I feel like the components folder is for generic React components while adding filters adds an unwanted dependency the
wp hooks.
I would follow that advice and remove the components.Autocomplete filter and introduce BlockAutocomplete component which uses components.Autocomplete.completers internally.
5c637d0 to
2bc0bf5
Compare
There was a problem hiding this comment.
So it looks like this issue with the obsolete div elements isn't something new. Let's leave it as it is and circle back later. We can investigate if React 16.3 has something to offer to mitigate it. https://reactjs.org/blog/2018/03/29/react-v-16-3.html - introduces createRef and forwardRef - they might be what we need in this case. We still need to expose those methods in a separate PR.
gziolo
left a comment
There was a problem hiding this comment.
I left another round of comments :)
docs/extensibility/autocompletion.md
Outdated
There was a problem hiding this comment.
Should be blocks not components :)
docs/extensibility/autocompletion.md
Outdated
There was a problem hiding this comment.
Should be blocks not components :)
docs/extensibility/autocompletion.md
Outdated
There was a problem hiding this comment.
Nit: Should we use acronym name to match with the variable name and description?
There was a problem hiding this comment.
I've struggled with whether to make name singular or plural. It feels weird to call it "acronym" because it is not an acronym, it completes acronyms. I ended up using plural for the user and block completers and wanted to be consistent here. I'm fine changing it to singular everywhere though. Any thoughts?
There was a problem hiding this comment.
Let’s keep it consistent. No need to update 👍
There was a problem hiding this comment.
Could you map other props like this?
const extraProps = [ 'className', 'allowNode', 'allowContext' ].
filter( key => key in deprecatedCompleter ).
map( key => deprecatedCompleter[ key ];
const compatibleCompleter = {
name: generateCompleterName(),
// ...
...extraProps,
};It seems like those functions don't add any new logic.
There was a problem hiding this comment.
Yep, that's more concise. I wanted to preserve completer context for the methods in that list, but Autocomplete already doesn't call them with completer context.
blocks/autocompleters/index.js
Outdated
components/autocomplete/README.md
Outdated
There was a problem hiding this comment.
It can be very brief - usually, one line is enough :)
There was a problem hiding this comment.
Adding a one line description works for me. :) I was thinking I needed to document props for the Autocomplete component as well since the README didn't yet exist, but that doesn't have to be part of this PR.
components/autocomplete/README.md
Outdated
There was a problem hiding this comment.
Can we also add a line where it is wired into an actual component?
There was a problem hiding this comment.
I created a simple example wiring to Autocomplete wrapping an input element, but it didn't work. It seems like Autocomplete is assuming a contenteditable target, since it inserts HTML rather than simple text. I'll play with a simple contenteditable example but don't have anything yet.
blocks/autocompleters/README.md
Outdated
There was a problem hiding this comment.
Can we add a reference to @wordpress/components? It contains the same content and will be difficult to keep in sync otherwise.
There was a problem hiding this comment.
I actually meant to remove that entirely after moving the content but missed doing so last night.
There was a problem hiding this comment.
Leaving a README with a reference to @wordpress/components seems good. Is there a better way than direct GitHub links to link to component READMEs?
blocks/autocomplete/index.js
Outdated
There was a problem hiding this comment.
Can you explain why we need this additional check in here? It seems like having onFocus handler should be enough.
There was a problem hiding this comment.
It is technically possible for the completers prop to change while the component contains the focus. This check exists for that case, since onFocus will already have been called. I don't think that is likely to occur, but I'd rather just cover the case. Sound OK?
There was a problem hiding this comment.
I hope it’s good 😃It makes sense, might make sense to leave an inline comment.
There was a problem hiding this comment.
It's mostly about not assuming when props will be updated. Sure, I can add a comment. :)
|
I noticed that this PR introduces regression for the following use cases:
|
|
Thanks for the last round of comments. I'm going through them and will look into the regressions. I'd noticed some awkwardness focusing the cite input but thought it was unrelated to this PR. I'm glad you caught it. |
36c1585 to
6174f58
Compare
|
I fixed the regressions and made updates in response to review feedback. I'd like to add a couple more automated tests, but this is ready for another round of manual testing. |
6174f58 to
70f9eb4
Compare
|
Looking good, I've tested it out and auto complete is working in lists, captions, quotes, heading and I assume other RichText components. Nice work! |
70f9eb4 to
c48f351
Compare
| link: 'https://github.com/WordPress/gutenberg/blob/master/components/autocomplete/README.md', | ||
| } ); | ||
|
|
||
| const optionalProperties = [ 'className', 'allowNode', 'allowContext' ] |
There was a problem hiding this comment.
It needs to be converted to an object, my fault :)
There was a problem hiding this comment.
I did the same thing. It's funny how our minds can just fill in the gaps sometimes. Thanks for fixing it.
|
I'm seeing the following error once per 20ish focus changes:
It doesn't break anything, but it would be nice to make sure it isn't triggered. |
gziolo
left a comment
There was a problem hiding this comment.
We went through so many iterations and it looks really solid. I tested it quite long today and didn't find any issues. It is a really great improvement 👍
I made sure that deprecation message is printed to the console when using old autocomplete intereface.
I also tested acronym completer example applied with hook. It works like a charm. It even gets applied just after I install it using JS console.
Great work. There are some random errors that might be not related to this PR, but it would be nice if you would try to reproduce them.
Time to merge 🎉
Sure, I'll play with this to see what I can find. Thanks for fixing the silly mistake with array spread instead of object spread in the completer compat module. Also thank you for all the time you gave reviewing and testing this. 🙇 |



Description
This is a PR for declaring and overriding autocompleters during block registration.
How Has This Been Tested?
I have manually tested with an ad hoc plugin, have added unit tests, and will be adding more.
Types of changes
typeproperty so they can be identified.onSelectfunction and replacing it withgetOptionCompletionthat returns an action/value pair. Currently supported actions areinsert-at-caretandreplace-block. This detangles completers from runtime dependencies on things such as theonReplacefunction. Now, theAutocompletercomponent uses the completion action to decide how to handle the result.getOptionCompletionfunction. This isn't a stance against the concept of value, but it seemed like unnecessary indirection in the current implementation.blocks/autocompleters.RichTextto include autocompletion support, including a newautocompletersprop that can be used to specify completers in place of the default.blocks/hooks/default-autocompletersmodule to provide a default list of completers for blocks that don't explicitly provide their own.blocks.Autocomplete.completersfilter is applied when theAutocompletecomponent is first focused after receiving a new list of completers.Checklist: