diff --git a/data/README.md b/data/README.md index d95d3d7cce2ca..ce2f42e09274d 100644 --- a/data/README.md +++ b/data/README.md @@ -22,4 +22,43 @@ Registers a [`listener`](https://redux.js.org/docs/api/Store.html#subscribe) fun #### `store.dispatch( action: object )` -The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch)object should be passed to this action. This action is passed to the registered reducers in addition to the previous state. +The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch) object should be passed to this function. This action is passed to the registered reducers in addition to the previous state. + + +### `wp.data.registerSelectors( reducerKey: string, newSelectors: object )` + +If your module or plugin needs to expose its state to other modules and plugins, you'll have to register state selectors. + +A selector is a function that takes the current state value as a first argument and extra arguments if needed and returns any data extracted from the state. + +#### Example: + +Let's say the state of our plugin (registered with the key `myPlugin`) has the following shape: `{ title: 'My post title' }`. We can register a `getTitle` selector to make this state value available like so: + +```js +wp.data.registerSelectors( 'myPlugin', { getTitle: ( state ) => state.title } ); +``` + +### `wp.data.select( key: string, selectorName: string, ...args )` + +This function allows calling any registered selector. Given a module's key, a selector's name and extra arguments passed to the selector, this function calls the selector passing it the current state and the extra arguments provided. + +#### Example: + +```js +wp.data.select( 'myPlugin', 'getTitle' ); // Returns "My post title" +``` + +### `wp.data.query( mapSelectorsToProps: func )( WrappedComponent: Component )` + +If you use a React or WordPress Element, a Higher Order Component is made available to inject data into your components like so: + +```js +const Component = ( { title } ) =>
{ title }
; + +wp.data.query( select => { + return { + title: select( 'myPlugin', 'getTitle' ), + }; +} )( Component ); +``` diff --git a/data/index.js b/data/index.js index 1cf4a779fd01a..841b5263ac054 100644 --- a/data/index.js +++ b/data/index.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import { connect } from 'react-redux'; import { createStore, combineReducers } from 'redux'; import { flowRight } from 'lodash'; @@ -8,6 +9,7 @@ import { flowRight } from 'lodash'; * Module constants */ const reducers = {}; +const selectors = {}; const enhancers = []; if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) { enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() ); @@ -17,17 +19,17 @@ const initialReducer = () => ( {} ); const store = createStore( initialReducer, {}, flowRight( enhancers ) ); /** - * Registers a new sub reducer to the global state and returns a Redux-like store object. + * Registers a new sub-reducer to the global state and returns a Redux-like store object. * - * @param {String} key Reducer key - * @param {Object} reducer Reducer function + * @param {string} reducerKey Reducer key. + * @param {Object} reducer Reducer function. * - * @returns {Object} Store Object. + * @returns {Object} Store Object. */ -export function registerReducer( key, reducer ) { - reducers[ key ] = reducer; +export function registerReducer( reducerKey, reducer ) { + reducers[ reducerKey ] = reducer; store.replaceReducer( combineReducers( reducers ) ); - const getState = () => store.getState()[ key ]; + const getState = () => store.getState()[ reducerKey ]; return { dispatch: store.dispatch, @@ -46,3 +48,55 @@ export function registerReducer( key, reducer ) { getState, }; } + +/** + * Registers selectors for external usage. + * + * @param {string} reducerKey Part of the state shape to register the + * selectors for. + * @param {Object} newSelectors Selectors to register. Keys will be used + * as the public facing API. Selectors will + * get passed the state as first argument. + */ +export function registerSelectors( reducerKey, newSelectors ) { + selectors[ reducerKey ] = newSelectors; +} + +/** + * Higher Order Component used to inject data using the registered selectors. + * + * @param {Function} mapSelectorsToProps Gets called with the selectors object + * to determine the data for the component. + * + * @returns {Func} Renders the wrapped component and passes it data. + */ +export const query = ( mapSelectorsToProps ) => ( WrappedComponent ) => { + const connectWithStore = ( ...args ) => { + const ConnectedWrappedComponent = connect( ...args )( WrappedComponent ); + return ( props ) => { + return ; + }; + }; + + return connectWithStore( ( state, ownProps ) => { + const select = ( key, selectorName, ...args ) => { + return selectors[ key ][ selectorName ]( state[ key ], ...args ); + }; + + return mapSelectorsToProps( select, ownProps ); + } ); +}; + +/** + * Calls a selector given the current state and extra arguments. + * + * @param {string} reducerKey Part of the state shape to register the + * selectors for. + * @param {string} selectorName Selector name. + * @param {*} args Selectors arguments. + * + * @returns {*} The selector's returned value. + */ +export const select = ( reducerKey, selectorName, ...args ) => { + return selectors[ reducerKey ][ selectorName ]( store.getState()[ reducerKey ], ...args ); +}; diff --git a/data/test/__snapshots__/index.js.snap b/data/test/__snapshots__/index.js.snap new file mode 100644 index 0000000000000..46758a91bd19a --- /dev/null +++ b/data/test/__snapshots__/index.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`query passes the relevant data to the component 1`] = ` +
+ reactState +
+`; diff --git a/data/test/index.js b/data/test/index.js index 4e0a2d68d4475..08d56fed3354d 100644 --- a/data/test/index.js +++ b/data/test/index.js @@ -1,4 +1,12 @@ -import { registerReducer } from '../'; +/** + * External dependencies + */ +import { render } from 'enzyme'; + +/** + * Internal dependencies + */ +import { registerReducer, registerSelectors, select, query } from '../'; describe( 'store', () => { it( 'Should append reducers to the state', () => { @@ -12,3 +20,42 @@ describe( 'store', () => { expect( store2.getState() ).toEqual( 'ribs' ); } ); } ); + +describe( 'select', () => { + it( 'registers multiple selectors to the public API', () => { + const store = registerReducer( 'reducer1', () => 'state1' ); + const selector1 = jest.fn( () => 'result1' ); + const selector2 = jest.fn( () => 'result2' ); + + registerSelectors( 'reducer1', { + selector1, + selector2, + } ); + + expect( select( 'reducer1', 'selector1' ) ).toEqual( 'result1' ); + expect( selector1 ).toBeCalledWith( store.getState() ); + + expect( select( 'reducer1', 'selector2' ) ).toEqual( 'result2' ); + expect( selector2 ).toBeCalledWith( store.getState() ); + } ); +} ); + +describe( 'query', () => { + it( 'passes the relevant data to the component', () => { + registerReducer( 'reactReducer', () => ( { reactKey: 'reactState' } ) ); + registerSelectors( 'reactReducer', { + reactSelector: ( state, key ) => state[ key ], + } ); + const Component = query( ( selectFunc, ownProps ) => { + return { + data: selectFunc( 'reactReducer', 'reactSelector', ownProps.keyName ), + }; + } )( ( props ) => { + return
{ props.data }
; + } ); + + const tree = render( ); + + expect( tree ).toMatchSnapshot(); + } ); +} ); diff --git a/editor/store/index.js b/editor/store/index.js index 4bb3318a78228..8da2e29e19aa9 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -1,7 +1,7 @@ /** * WordPress Dependencies */ -import { registerReducer } from '@wordpress/data'; +import { registerReducer, registerSelectors } from '@wordpress/data'; /** * Internal dependencies @@ -12,11 +12,13 @@ import { withRehydratation, loadAndPersist } from './persist'; import enhanceWithBrowserSize from './mobile'; import applyMiddlewares from './middlewares'; import { BREAK_MEDIUM } from './constants'; +import { getEditedPostTitle } from './selectors'; /** * Module Constants */ const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`; +const MODULE_KEY = 'core/editor'; const store = applyMiddlewares( registerReducer( 'core/editor', withRehydratation( reducer, 'preferences' ) ) @@ -24,4 +26,6 @@ const store = applyMiddlewares( loadAndPersist( store, 'preferences', STORAGE_KEY, PREFERENCES_DEFAULTS ); enhanceWithBrowserSize( store, BREAK_MEDIUM ); +registerSelectors( MODULE_KEY, { getEditedPostTitle } ); + export default store; diff --git a/lib/client-assets.php b/lib/client-assets.php index a4f787ade8954..240d5793d34e3 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -79,7 +79,7 @@ function gutenberg_register_scripts_and_styles() { wp_register_script( 'wp-data', gutenberg_url( 'data/build/index.js' ), - array(), + array( 'wp-element' ), filemtime( gutenberg_dir_path() . 'data/build/index.js' ) ); wp_register_script(