Skip to content

Commit

Permalink
Framework: Introduce the data module (#3832)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored Dec 18, 2017
1 parent 7c917c9 commit 5c33f6f
Show file tree
Hide file tree
Showing 27 changed files with 291 additions and 128 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@
"selector": "ImportDeclaration[source.value=/^i18n$/]",
"message": "Use @wordpress/i18n as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^data$/]",
"message": "Use @wordpress/data as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^utils$/]",
"message": "Use @wordpress/utils as import path instead."
Expand Down
1 change: 1 addition & 0 deletions bin/build-plugin-zip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ zip -r gutenberg.zip \
element/build/*.{js,map} \
hooks/build/*.{js,map} \
i18n/build/*.{js,map} \
data/build/*.{js,map} \
utils/build/*.{js,map} \
blocks/build/*.css \
components/build/*.css \
Expand Down
26 changes: 26 additions & 0 deletions data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Data
====

The more WordPress UI moves to the client, the more there's a need for a centralized data module allowing data management and sharing between several WordPress modules and plugins.

This module holds a global state variable and exposes a "Redux-like" API containing the following methods:


### `wp.data.registerReducer( reducer: function )`

If your module or plugin needs to store and manipulate client-side data, you'll have to register a "reducer" to do so. A reducer is a function taking the previous `state` and `action` and returns an update `state`. You can learn more about reducers on the [Redux Documentation](https://redux.js.org/docs/basics/Reducers.html)

### `wp.data.getState()`

A simple function to returns the JS object containing the state of all the WP Modules.
This function is present for convenience to use things like `react-redux`.
You should not use rely on other modules state since the state object's shape may change over time breaking your module.
An official way to expose your module's state will be added later.

### `wp.data.subscribe( listener: function )`

Registers a `listener` function called everytime the state is updated.

### `wp.data.dispatch( action: object )`

The dispatch function should be called to trigger the registered reducers function and update the state. An `action` object should be passed to this action. This action is passed to the registered reducers in addition to the previous state.
34 changes: 34 additions & 0 deletions data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { createStore, combineReducers } from 'redux';
import { flowRight } from 'lodash';

/**
* Module constants
*/
const reducers = {};
const enhancers = [];
if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) {
enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() );
}

const initialReducer = () => ( {} );
const store = createStore( initialReducer, {}, flowRight( enhancers ) );

/**
* Registers a new sub reducer to the global state
*
* @param {String} key Reducer key
* @param {Object} reducer Reducer function
*/
export function registerReducer( key, reducer ) {
reducers[ key ] = reducer;
store.replaceReducer( combineReducers( reducers ) );
}

export const subscribe = store.subscribe;

export const dispatch = store.dispatch;

export const getState = store.getState;
17 changes: 17 additions & 0 deletions data/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { registerReducer, getState } from '../';

describe( 'store', () => {
it( 'Should append reducers to the state', () => {
const reducer1 = () => 'chicken';
const reducer2 = () => 'ribs';

registerReducer( 'red1', reducer1 );
expect( getState() ).toEqual( { red1: 'chicken' } );

registerReducer( 'red2', reducer2 );
expect( getState() ).toEqual( {
red1: 'chicken',
red2: 'ribs',
} );
} );
} );
2 changes: 1 addition & 1 deletion docs/coding-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import TinyMCE from 'tinymce';

#### WordPress Dependencies

To encourage reusability between features, our JavaScript is split into domain-specific modules which [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) one or more functions or objects. In the Gutenberg project, we've distinguished these modules under top-level directories `blocks`, `components`, `editor`, `element`, and `i18n`. These each serve an independent purpose, and often code is shared between them. For example, in order to localize its text, editor code will need to include functions from the `i18n` module.
To encourage reusability between features, our JavaScript is split into domain-specific modules which [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) one or more functions or objects. In the Gutenberg project, we've distinguished these modules under top-level directories `blocks`, `components`, `editor`, `element`, `data` and `i18n`. These each serve an independent purpose, and often code is shared between them. For example, in order to localize its text, editor code will need to include functions from the `i18n` module.

Example:

Expand Down
2 changes: 1 addition & 1 deletion editor/components/error-boundary/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ErrorBoundary extends Component {
}

reboot() {
this.props.onError( this.context.store.getState() );
this.props.onError();
}

getContent() {
Expand Down
11 changes: 5 additions & 6 deletions editor/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
* Internal Dependencies
*/
import { setupEditor, undo } from '../../actions';
import createReduxStore from '../../store';
import store from '../../store';

/**
* The default editor settings
Expand All @@ -42,15 +42,15 @@ class EditorProvider extends Component {
constructor( props ) {
super( ...arguments );

this.store = createReduxStore( props.initialState );
this.store = store;

this.settings = {
...DEFAULT_SETTINGS,
...props.settings,
};

// If initial state is passed, assume that we don't need to initialize,
// as in the case of an error recovery.
if ( ! props.initialState ) {
// Assume that we don't need to initialize in the case of an error recovery.
if ( ! props.recovery ) {
this.store.dispatch( setupEditor( props.post, this.settings ) );
}
}
Expand All @@ -63,7 +63,6 @@ class EditorProvider extends Component {

componentWillReceiveProps( nextProps ) {
if (
nextProps.store !== this.props.store ||
nextProps.settings !== this.props.settings
) {
// eslint-disable-next-line no-console
Expand Down
10 changes: 5 additions & 5 deletions editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { EditorProvider, ErrorBoundary } from './components';
import { initializeMetaBoxState } from './actions';

export * from './components';
import store from './store'; // Registers the state tree

// Configure moment globally
moment.locale( dateSettings.l10n.locale );
Expand Down Expand Up @@ -52,15 +53,14 @@ window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => {
*
* @param {Element} target DOM node in which editor is rendered
* @param {?Object} settings Editor settings object
* @param {*} initialState Initial editor state to hydrate
*/
export function recreateEditorInstance( target, settings, initialState ) {
export function recreateEditorInstance( target, settings ) {
unmountComponentAtNode( target );

const reboot = recreateEditorInstance.bind( null, target, settings );

render(
<EditorProvider settings={ settings } initialState={ initialState }>
<EditorProvider settings={ settings } recovery>
<ErrorBoundary onError={ reboot }>
<Layout />
</ErrorBoundary>
Expand All @@ -84,7 +84,7 @@ export function createEditorInstance( id, post, settings ) {
const target = document.getElementById( id );
const reboot = recreateEditorInstance.bind( null, target, settings );

const provider = render(
render(
<EditorProvider settings={ settings } post={ post }>
<ErrorBoundary onError={ reboot }>
<Layout />
Expand All @@ -95,7 +95,7 @@ export function createEditorInstance( id, post, settings ) {

return {
initializeMetaBoxes( metaBoxes ) {
provider.store.dispatch( initializeMetaBoxState( metaBoxes ) );
store.dispatch( initializeMetaBoxState( metaBoxes ) );
},
};
}
2 changes: 1 addition & 1 deletion editor/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import { POST_UPDATE_TRANSACTION_ID } from './effects';
import { BREAK_MEDIUM } from './constants';

/***
* Module constants
*/
export const POST_UPDATE_TRANSACTION_ID = 'post-update';
const MAX_FREQUENT_BLOCKS = 3;

/**
Expand Down
51 changes: 0 additions & 51 deletions editor/store.js

This file was deleted.

File renamed without changes.
File renamed without changes.
11 changes: 7 additions & 4 deletions editor/effects.js → editor/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getPostEditUrl, getWPAdminURL } from './utils/url';
import { getPostEditUrl, getWPAdminURL } from '../utils/url';
import {
resetPost,
setupNewPost,
Expand All @@ -37,7 +37,7 @@ import {
updateReusableBlock,
saveReusableBlock,
insertBlock,
} from './actions';
} from '../actions';
import {
getCurrentPost,
getCurrentPostType,
Expand All @@ -50,12 +50,15 @@ import {
isEditedPostSaveable,
getBlock,
getReusableBlock,
} from './selectors';
POST_UPDATE_TRANSACTION_ID,
} from '../selectors';

/**
* Module Constants
*/
const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID';
const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID';
const SAVE_REUSABLE_BLOCK_NOTICE_ID = 'SAVE_REUSABLE_BLOCK_NOTICE_ID';
export const POST_UPDATE_TRANSACTION_ID = 'post-update';

export default {
REQUEST_POST_UPDATE( action, store ) {
Expand Down
24 changes: 24 additions & 0 deletions editor/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* WordPress Dependencies
*/
import { registerReducer } from '@wordpress/data';

/**
* Internal dependencies
*/
import { PREFERENCES_DEFAULTS } from './defaults';
import reducer from './reducer';
import { withRehydratation, loadAndPersist } from './persist';
import enhanceWithBrowserSize from './browser';
import store from './store';

/**
* Module Constants
*/
const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`;

registerReducer( 'core/editor', withRehydratation( reducer, 'preferences' ) );
loadAndPersist( store, 'preferences', STORAGE_KEY, PREFERENCES_DEFAULTS );
enhanceWithBrowserSize( store );

export default store;
66 changes: 66 additions & 0 deletions editor/store/persist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* Adds the rehydratation behavior to redux reducers
*
* @param {Function} reducer The reducer to enhance
* @param {String} reducerKey The reducer key to persist
*
* @return {Function} Enhanced reducer
*/
export function withRehydratation( reducer, reducerKey ) {
// EnhancedReducer with auto-rehydration
const enhancedReducer = ( state, action ) => {
const nextState = reducer( state, action );

if ( action.type === 'REDUX_REHYDRATE' ) {
return {
...nextState,
[ reducerKey ]: action.payload,
};
}

return nextState;
};

return enhancedReducer;
}

/**
* Loads the initial state and persist on changes
*
* This should be executed after the reducer's registration
*
* @param {Object} store Store to enhance
* @param {String} reducerKey The reducer key to persist (example: reducerKey.subReducerKey)
* @param {String} storageKey The storage key to use
* @param {Object} defaults Default values of the reducer key
*/
export function loadAndPersist( store, reducerKey, storageKey, defaults = {} ) {
// Load initially persisted value
const persistedString = window.localStorage.getItem( storageKey );
if ( persistedString ) {
const persistedState = {
...defaults,
...JSON.parse( persistedString ),
};

store.dispatch( {
type: 'REDUX_REHYDRATE',
payload: persistedState,
} );
}

// Persist updated preferences
let currentStateValue = get( store.getState(), reducerKey );
store.subscribe( () => {
const newStateValue = get( store.getState(), reducerKey );
if ( newStateValue !== currentStateValue ) {
currentStateValue = newStateValue;
window.localStorage.setItem( storageKey, JSON.stringify( currentStateValue ) );
}
} );
}
Loading

0 comments on commit 5c33f6f

Please sign in to comment.