Skip to content

Commit

Permalink
Merge pull request #4016 from WordPress/add/with-state
Browse files Browse the repository at this point in the history
Components: Add withState higher-order component
  • Loading branch information
aduth authored Dec 15, 2017
2 parents 5d81a0a + 925acf9 commit 7cb404d
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 54 deletions.
90 changes: 36 additions & 54 deletions blocks/library/html/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { withState } from '@wordpress/components';

/**
* Internal dependencies
Expand Down Expand Up @@ -41,60 +41,42 @@ registerBlockType( 'core/html', {
},
},

edit: class extends Component {
constructor() {
super( ...arguments );
this.preview = this.preview.bind( this );
this.edit = this.edit.bind( this );
this.state = {
preview: false,
};
}

preview() {
this.setState( { preview: true } );
}

edit() {
this.setState( { preview: false } );
}

render() {
const { preview } = this.state;
const { attributes, setAttributes, focus } = this.props;

return (
<div>
{ focus &&
<BlockControls key="controls">
<div className="components-toolbar">
<button className={ `components-tab-button ${ ! preview ? 'is-active' : '' }` } onClick={ this.edit }>
<span>HTML</span>
</button>
<button className={ `components-tab-button ${ preview ? 'is-active' : '' }` } onClick={ this.preview }>
<span>{ __( 'Preview' ) }</span>
</button>
</div>
</BlockControls>
}
{ preview ?
<div dangerouslySetInnerHTML={ { __html: attributes.content } } /> :
<TextareaAutosize
value={ attributes.content }
onChange={ ( event ) => setAttributes( { content: event.target.value } ) }
/>
}
{ focus &&
<InspectorControls key="inspector">
<BlockDescription>
<p>{ __( 'Add custom HTML code and preview it right here in the editor.' ) }</p>
</BlockDescription>
</InspectorControls>
}
edit: withState( {
preview: false,
} )( ( { attributes, setAttributes, setState, focus, preview } ) => [
focus && (
<BlockControls key="controls">
<div className="components-toolbar">
<button
className={ `components-tab-button ${ ! preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: false } ) }>
<span>HTML</span>
</button>
<button
className={ `components-tab-button ${ preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: true } ) }>
<span>{ __( 'Preview' ) }</span>
</button>
</div>
);
}
},
</BlockControls>
),
preview ?
<div
key="preview"
dangerouslySetInnerHTML={ { __html: attributes.content } } /> :
<TextareaAutosize
key="editor"
value={ attributes.content }
onChange={ ( event ) => setAttributes( { content: event.target.value } ) }
/>,
focus && (
<InspectorControls key="inspector">
<BlockDescription>
<p>{ __( 'Add custom HTML code and preview it right here in the editor.' ) }</p>
</BlockDescription>
</InspectorControls>
),
] ),

save( { attributes } ) {
return attributes.content;
Expand Down
32 changes: 32 additions & 0 deletions components/higher-order/with-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
withState
=========

`withState` is a React [higher-order component](https://facebook.github.io/react/docs/higher-order-components.html) which enables a function component to have internal state.

Wrapping a component with `withState` provides state as props to the wrapped component, along with a `setState` updater function.

## Usage

```jsx
/**
* WordPress dependencies
*/
import { withState } from '@wordpress/components';

function MyCounter( { count, setState } ) {
return (
<>
Count: { count }
<button onClick={ () => setState( ( state ) => ( { count: state.count + 1 } ) ) }>
Increment
</button>
</>
);
}

export default withState( {
count: 0,
} )( MyCounter );
```

`withState` optionally accepts an object argument to define the initial state. It returns a function which can then be used in composing your component.
41 changes: 41 additions & 0 deletions components/higher-order/with-state/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* WordPress dependencies
*/
import { Component, getWrapperDisplayName } from '@wordpress/element';

/**
* A Higher Order Component used to provide and manage internal component state
* via props.
*
* @param {?Object} initialState Optional initial state of the component
* @return {Component} Wrapped component
*/
function withState( initialState = {} ) {
return ( OriginalComponent ) => {
class WrappedComponent extends Component {
constructor() {
super( ...arguments );

this.setState = this.setState.bind( this );

this.state = initialState;
}

render() {
return (
<OriginalComponent
{ ...this.props }
{ ...this.state }
setState={ this.setState }
/>
);
}
}

WrappedComponent.displayName = getWrapperDisplayName( WrappedComponent, 'state' );

return WrappedComponent;
};
}

export default withState;
25 changes: 25 additions & 0 deletions components/higher-order/with-state/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { mount } from 'enzyme';

/**
* Internal dependencies
*/
import withState from '../';

describe( 'withState', () => {
it( 'should pass initial state and allow updates', () => {
const EnhancedComponent = withState( { count: 0 } )( ( { count, setState } ) => (
<button onClick={ () => setState( ( state ) => ( { count: state.count + 1 } ) ) }>
{ count }
</button>
) );

const wrapper = mount( <EnhancedComponent /> );

expect( wrapper.html() ).toBe( '<button>0</button>' );
wrapper.find( 'button' ).simulate( 'click' );
expect( wrapper.html() ).toBe( '<button>1</button>' );
} );
} );
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ export { default as withFocusOutside } from './higher-order/with-focus-outside';
export { default as withFocusReturn } from './higher-order/with-focus-return';
export { default as withInstanceId } from './higher-order/with-instance-id';
export { default as withSpokenMessages } from './higher-order/with-spoken-messages';
export { default as withState } from './higher-order/with-state';

0 comments on commit 7cb404d

Please sign in to comment.