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

Components: Add withState higher-order component #4016

Merged
merged 2 commits into from
Dec 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 } ) => [
Copy link
Member

@gziolo gziolo Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start using React.Fragment to avoid using keys? Do we want to update Babel first? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start using React.Fragment to avoid using keys? Do we want to update Babel first? :)

Yeah, I'd probably want to avoid the direct reference to React.Fragment, but if we had <> syntax available, I'd be open to using it. Maybe in a subsequent pull request where we upgrade Babel?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can spin up PR with Babel upgrade on Monday :)

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';