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

API for Inter-block communication #5012

Closed
Shelob9 opened this issue Feb 12, 2018 · 16 comments
Closed

API for Inter-block communication #5012

Shelob9 opened this issue Feb 12, 2018 · 16 comments
Labels
[Feature] Extensibility The ability to extend blocks or the editing experience [Priority] High Used to indicate top priority items that need quick attention

Comments

@Shelob9
Copy link
Contributor

Shelob9 commented Feb 12, 2018

I made this trivial example of how two blocks can communicate between each other. https://gitlab.com/caldera-labs/gutenberg-examples/ex-6-counter It is Redux's counter example in WordPress.

@gziolo asked me to open an issue with use cases where plugin developers need to subscribe to changes in the editor or subscribe to changes in each other's state. We discussed it here

Specifically @gziolo said:

The only limitation with the way you dispatch actions as you did is that you don’t have access to other modules. That’s why I raised it. I think we need a high level API which will allow to do some actions programmatically. I guess someone needs to provide a valid use case first

I gave the example of a client site I had that needed to populate WooCommerce's metabox with data from an API lookup based on one field's value. I used jQuery.on() and jQuery.val() a lot on that job. With Gutenberg, it would be crazy for me as a plugin developer to use the DOM as the single source of truth for state. I want to subscribe to state through wp.data, which I can.

Another example would be if we had a custom block for information about a musical artist. We might also want to develop a custom block that extended the default SoundCloud block, but had a subscription to the artist block. We'd want our extended SoundCloud block to update to a playlist of that artist, if the custom artist block existed.

Relationships between posts of different types -- books to authors -- is something that plugins like Pods or Posts 2 Posts do. The UI is tricky, since you really want to edit two posts at once. In the author metabox of the books post type, you should be able to select and edit existing authors and create new authors.

With Gutenberg, I'd like to be able to develop a set of blocks for books and a set of blocks for authors. The set of blocks I'm building for books will need to include an author block, I'd like to use the same components to construct the book author block as I do constructing the blocks I need for the author post type.

As a plugin developer I need an API that provides for inter-block communication and is consistently shaped no matter which of these two contexts I'm in. Also, once this API is well defined, I can use it with other instances of wp.data or of Redux, which makes my components potentially useful in the front-end, which is where this gets even more fun :)

Related for context:

@Shelob9
Copy link
Contributor Author

Shelob9 commented Feb 12, 2018

I should also add, that these selectors are amazing to have: https://github.com/WordPress/gutenberg/blob/master/editor/store/index.js#L14

I would love to also be able to access all of the post's taxonomy's terms. That would be so cool. For example, I could dynamically populate embeds of related content - or suggest them - based on categories and tags as they change.

@gziolo gziolo added the [Feature] Extensibility The ability to extend blocks or the editing experience label Feb 13, 2018
@gziolo
Copy link
Member

gziolo commented Feb 13, 2018

@Shelob9, thanks for opening this issue and your detailed description. I think we both are on the same page in terms how data module should evolve. It should be possible to dispatch actions from other modules using a similar API that we expose for selectors. @youknowriad any thoughts on that?

@youknowriad
Copy link
Contributor

Maybe I'm not understanding properly but it seems that there's no need for another API to achieve this. The query HoC should be enough if we expose the getBlocks selector.

A plugin should be able to filter this list of blocks to get attributes from the desired related block.

@gziolo
Copy link
Member

gziolo commented Feb 13, 2018

import counter from '../counter';
...
              <button
                    //When button is increased, increase count
                    onClick={() => counter.dispatch({ type: 'INCREMENT' })}
                >
...

@youknowriad - the other parts is related to dispatching actions. It is possible to dispatch an action from the store you own but we don't have any abstraction that mirrors mapDispatchToProps from Redux. It isn't possible to dispatch an action to other stores because we use multi-store approach where every store is isolated. It would be nice to have a way to expose actions for public consumption similar to what we already offer for selectors. This would allow to programmatically create new block, update their attributes, change focus, update post's meta attributes.

@Shelob9
Copy link
Contributor Author

Shelob9 commented Feb 13, 2018

RE: @gziolo This would allow to programmatically create new block, update their attributes, change focus, update post's meta attributes. Yes. That's what I want.

@youknowriad said:

Maybe I'm not understanding properly but it seems that there's no need for another API to achieve this. The query HoC should be enough if we expose the getBlocks selector.
I think the query HoC is pretty close to what we need.

Is the answer to these questions, yes? If so, then we're largely covered.

  • Can I subscribe to taxonomy terms with select( 'core/editor', 'getEditedPostAttribute', 'terms' ) or similar?
  • Can I subscribe to the changes in another blocks attributes? IE What if I want to automatically add tags to the post based on the attachments currently set in every image block, if there are image blocks?

@Shelob9
Copy link
Contributor Author

Shelob9 commented Feb 13, 2018

Another part of this -- more a documentation issue -- is that I may want a private store. IE I want to be able to communicate between my own blocks, without allowing

I think the solution here would be to register my own wp.data or wp.hooks instance in my plugin and then use that into all of my components.

@JasonHoffmann
Copy link
Contributor

JasonHoffmann commented Feb 14, 2018

Moving from over here: #5006 (comment)

My use case is when there is an initial configuration object that I want to be editable by other developers if they'd like to extend my block further.

For instance (just a hypothetical) let's say I have an "Aspect Ratio Responsive Image Block." In the Inspector Controls I have a single dropdown menu which has a list of aspect ratios to choose from. However, I'd like to also allow other developers to make their own plugin, extend out my block, and add their own aspect ratios to that dropdown menu (or remove existing ones).

The way I was trying to solve this was to create the initial default list, the variable aspect_ratios, and then pass it through wp.hooks so that it is filterable, similar to what I might do in PHP. That was causing me with some issues with execution that do seem solveable. But I would prefer a more unified approach. If I was able to pass the aspect_ratios array up to wp.data instead, and then other developers could subscribe to it and modify that array before I go and grab it again in the save and edit functions of my block, that would be preferable. It is quite a bit neater then trying to manage whether or not a hook has been called yet, and it centralizes data enough to be portable.

So this would require the ability to subscribe to changes in other blocks and make modifications to data there.

I'm not sure if this is currently possible with a mix of reducers and wp.data.query objects, but I can't seem to implement something that would work. It would be nice if the API was (I know this is oversimplified):

const myArray = [ ... ];
wp.data.registerData( 'filterableArray', myArray );

And then modifying that data elsewhere could be something like:

wp.data.filterData( function( myArray ) {
  myArray.push( 'new item' );
  return myArray;
} );

That's really rough, but I think it gives some idea of what I'm thinking. If I'm off-base and there's a way to do this now without much difficulty I'd be happy to use that approach. But can't seem to find one that works at the moment.

@Shelob9
Copy link
Contributor Author

Shelob9 commented Feb 14, 2018

@JasonHoffmann Thank you. I think you're getting at why, even though what you need is pretty much possible right now -- at master, not current version -- this is still worth discussing.

So, you almost have problem solved with wp.data, see this example: https://gitlab.com/caldera-labs/gutenberg-examples/ex-6-counter/blob/master/src/counter.js That's making a new store, just an integer in this case, but could be an array of aspect ratios available to a component, like the one in your inspector controls.

Ok, fine, but how does my plugin extending yours add an aspect ratio, or make it so 4:3 aspect ratio is never acceptable? Do I dispatch an action you registered in your store? I think, but how do I know you've registered this action?

How do you make sure that I don't push some totally invalid data into your array? Whose responsibility is it to validate the data?

Also, as a plugin developer extending your plugin how do I discover the shape of your selectors and what selectors you'd made available?

I don't know the Redux ecosystem well enough to know how much of this can be solved through existing tools. I do know that the serious weakness in the PHP WordPress hooks is that, opposed to a modern, object-oriented event framework, we can't discovery parameters of events, or enforcing types on the return values. I develop a popular plugin that has tons of add-ons and site-specific code assuming our hooks work a certain way. Developing extensible software around assumptions is in my opinion, not as good as having declarative and discoverable APIs, like the one being created here.

#TL;DR
How can we refactor @JasonHoffmann pseudo-code so that if I extend the store from this plugin, I can I programmatically discover what the shape of its selectors are, and what data (including types) it's exposing, and what actions can I use to change it?

@gziolo
Copy link
Member

gziolo commented Feb 15, 2018

Can I subscribe to taxonomy terms with select( 'core/editor', 'getEditedPostAttribute', 'terms' ) or similar?

Please note that API has slightly changed to select( 'core/editor' ).getEditedPostAttribute( 'terms' ), but in theorhy this should work if it is stored already in the state. I recommend using Redux dev tools which is enabled in Gutenberg and exploring state to see what is there. However as probably commented somewhere else, you should be aware that we might want to whitelist what is exposed from the state in the future. It's still under heavy development as discussed already :)

Can I subscribe to the changes in another blocks attributes? IE What if I want to automatically add tags to the post based on the attachments currently set in every image block, if there are image blocks?

Attributes are stored inside Redux state, too. So it should be possible to open for public usage selector which fetches this data and then you could use wp.data.subscribe to detect if they have changed. I think @aduth answered to a similar question yesterday on Slack: https://wordpress.slack.com/archives/C02QB2JS7/p1518634800000234.

@gziolo
Copy link
Member

gziolo commented Feb 15, 2018

Another part of this -- more a documentation issue -- is that I may want a private store. IE I want to be able to communicate between my own blocks, without allowing

I think the solution here would be to register my own wp.data or wp.hooks instance in my plugin and then use that into all of my components.

wp.data.registerReducer as described here returns the store instance. If you don't register any selectors, then it will remain private. You would be still able to use it internally using store instance returned by the aforementioned function. You would have to use store.getState to get data from your state, store.subscribe to detect changes in your state and store.dispatch to trigger updates in your reducer. It isn't designed to work this way, but it is available for the use cases you asked about.

@gziolo
Copy link
Member

gziolo commented Feb 15, 2018

How can we refactor @JasonHoffmann pseudo-code so that if I extend the store from this plugin, I can I programmatically discover what the shape of its selectors are, and what data (including types) it's exposing, and what actions can I use to change it?

I think that you can at least browse available selectors after @aduth refactored wp.data.select API with #5007. We don't use any type system in Gutenberg, so you can't detect what params need to be provided, what types they have and what is the shape of the return value. It all needs to be documented. The benefit of using Redux is that you can enforce a certain shape of your state by coding transformation for every action dispatched. What it means is that you specify a set of actions which allow to interact with your store and you know exactly how it is going to update your state. In case of hooks and filters in particular, you could, in theory, add some validation level, but it would need to be done after the fact. So you could rollback corrupted changes, but this wouldn't happen for all applied filter as far as I understand. If you operate on plain data that JS can serialize and you want to share it with a few component or plugins then wp.data is a better choice in my opinion.

@JasonHoffmann
Copy link
Contributor

JasonHoffmann commented Feb 15, 2018

I'm still a bit unclear about how to use wp.data to allow other plugins to hook into wp.data and change something.

I've posted a gist of what I'm working on, which is a pretty basic Syntax Highlighting block. I have a list of languages in an array, codeMap, that would be available but I'd want other plugins to be able to register their own to the array if they wanted to add one:
https://gist.github.com/JasonHoffmann/31a492aaf6f3b244b9e32ce1fe8526c9

index.js is a simplified version of the main block and addon.js is a sample implementation that would be added inside of another plugin. It's using wp.hooks but would there be a way to replicate this with wp.data. I'm not sure if it would be possible for the addon.js file to register it's own reducer and somehow return a mutated state. But I know that addon.js would not be able to dispatch an action on an existing reducer, since it would have no access to it unless I made that globally available. Am I missing something?

@Shelob9
Copy link
Contributor Author

Shelob9 commented Feb 16, 2018

@gizlo This is amazing information. I still don't get 100% the best way for the add-on plugin to push new options into the store created with wp.data. I have two code examples at the end that I think work. BTW @JasonHoffmann Looking at your code, I really don't think you shouldn't be using wp.hooks.

@JasonHoffmann I found reading this to be really helpful when learning Redux in a WordPress context https://github.com/WordPress/gutenberg/blob/master/data/index.js

@youknowriad This feels exactly like the kind of thing you were talking about when I asked if the example code for the tutorial I'm working about felt right.

So some pseudo-tutorial that I think summarizes what we've learned here. If these paragraphs and outline are correct, then I'll write this up for real, once this release is done.

Writing Extensible Blocks

Let's say there are two plugins. One is a "core" plugin, the other is an "add-on". The "core" plugin is some sort of video player plugin that has a block for selecting the video aspect ratio. The "add-on" plugin that adds support for new aspect ratios. In order to make the aspect ratio feature extensible, the list of available aspect ratios used in the aspect ratios selectors needs some way for the add-on plugin to add aspect ratios.

First, the core plugin will need to move the array of aspect ratios into a "store" registered with wp.data. This requires, registering a reducer function, then registering a selector with the same reducer key. Doing so, will make it possible for the aspect ratio block to be wrapped in a query for the aspect ratios selector, and for the add-on plugin to push new aspect ratios into that array.

So, I started this way, which ended poorly:

    //Subscribe to changes in aspect ratios
    const unsubscribe = wp.data.subscribe( () => {
        //Get the aspect ratios
        let aspectRatios = wp.data.select( 'videoPlugin/aspectRatios' );
        //This conditional protects against a sitation where aspectRatios is not yet or will never be an array
        //Right?
        if ( aspectRatios ) {
            aspectRatios.push(['4:3']);
            //Dispatch the action?
            //How? Why? Is it even possible? What is life really?
        }
    } );
    
    // Unsubcribe, so this runs once.
    // How do we know it has run once?
    unsubscribe();

So that was a bad idea I think, but then I re-read the Redux docs on side effects because of all of your comments and what Riad keeps saying.

So I want to refactor the above code example, which probably would never would work based on this answer from The Oracle

    // actions.js
    export function addAspectRatio(ratio) {
        return { type: 'ADD_ASPECT_RATIO', ratio }
    }
    
    // add-on.js
    import { addAspectRatio } from '../actions'

    this.props.dispatch(addAspectRatio('4:3' ) );

@gziolo
Copy link
Member

gziolo commented Feb 19, 2018

I've posted a gist of what I'm working on, which is a pretty basic Syntax Highlighting block. I have a list of languages in an array, codeMap, that would be available but I'd want other plugins to be able to register their own to the array if they wanted to add one:
https://gist.github.com/JasonHoffmann/31a492aaf6f3b244b9e32ce1fe8526c9

@JasonHoffmann this is more or less how it should look like when using wp.hooks. The issue here is that you share the same instance of the array between calls so it appends data registered with filters on every rerender of edit component and every time block's HTML gets stored through save component. This is the updated version which should work as expected:
https://gist.github.com/gziolo/c03027da16ed63e5e6e7447cc5d357fa/revisions
The most important change added there is: it operated on a fresh copy of codeMap to avoid mutation.
It can be further optimized by using once from Lodash or as explained in this comment: #1732 (comment). It would make sure it is computed only once and then reused on every further call.

I have already explained everything in depth in one of my previous comments. Linking here in case you missed: #5006 (comment).

@gziolo
Copy link
Member

gziolo commented Feb 19, 2018

@Shelob9 I think all that you wrote is going to be further simplified once wp.data.dispatch API proposed by @youknowriad in #5137 is going to be merged. Let's revisit your comment once the data module is updated :)

@gziolo gziolo added the [Priority] High Used to indicate top priority items that need quick attention label Feb 28, 2018
@gziolo
Copy link
Member

gziolo commented Feb 28, 2018

I'm closing this one as we improved Data module in the recent days. Full documentation lives under: https://github.com/WordPress/gutenberg/tree/master/data.

All actions that core uses should be now exposed for all plugins that would like to take advantage of them. The same applies to selectors. This opens a magnitude of possibilities when trying to extend editor with plugins.

Let me also summarize new additions that are the most important the context of our discussion in this issue. We added the following methods:

  • wp.data.registerActions( reducerKey: string, newActions: object ) - allows to register your own actions to make them available for other plugins
  • wp.data.dispatch( key: string ) - allows calling any registered action
  • wp.data.withDispatch( propsToDispatchers: Object )( WrappedComponent: Component ) - to manipulate store data, you can also pass dispatching actions into your component as props

If there are more question, I'm happy to answer them in here. If there is something else that we should consider to make the proposed API more flexible, please open a new issue to make discussion focused on the proposed concept.

@gziolo gziolo closed this as completed Feb 28, 2018
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 [Priority] High Used to indicate top priority items that need quick attention
Projects
None yet
Development

No branches or pull requests

4 participants