Skip to content

Commit

Permalink
Components: Force update when new filter added or removed when using …
Browse files Browse the repository at this point in the history
…withFilters HOC (#4428)
  • Loading branch information
gziolo committed Jan 18, 2018
1 parent cda269d commit e9f38ae
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 8 deletions.
42 changes: 40 additions & 2 deletions components/higher-order/with-filters/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,63 @@
/**
* External dependencies
*/
import { debounce, uniqueId } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, getWrapperDisplayName } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';
import { addAction, applyFilters, removeAction } from '@wordpress/hooks';

const ANIMATION_FRAME_PERIOD = 16;

/**
* Creates a higher-order component which adds filtering capability to the wrapped component.
* Filters get applied when the original component is about to be mounted.
* When a filter is added or removed that matches the hook name, the wrapped component re-renders.
*
* @param {String} hookName Hook name exposed to be used by filters.
* @param {string} hookName Hook name exposed to be used by filters.
*
* @returns {Function} Higher-order component factory.
*/
export default function withFilters( hookName ) {
return ( OriginalComponent ) => {
class FilteredComponent extends Component {
/** @inheritdoc */
constructor( props ) {
super( props );

this.onHooksUpdated = this.onHooksUpdated.bind( this );
this.Component = applyFilters( hookName, OriginalComponent );
this.namespace = uniqueId( 'core/with-filters/component-' );
this.throttledForceUpdate = debounce( () => {
this.Component = applyFilters( hookName, OriginalComponent );
this.forceUpdate();
}, ANIMATION_FRAME_PERIOD );

addAction( 'hookRemoved', this.namespace, this.onHooksUpdated );
addAction( 'hookAdded', this.namespace, this.onHooksUpdated );
}

/** @inheritdoc */
componentWillUnmount() {
this.throttledForceUpdate.cancel();
removeAction( 'hookRemoved', this.namespace );
removeAction( 'hookAdded', this.namespace );
}

/**
* When a filter is added or removed for the matching hook name, the wrapped component should re-render.
*
* @param {string} updatedHookName Name of the hook that was updated.
*/
onHooksUpdated( updatedHookName ) {
if ( updatedHookName === hookName ) {
this.throttledForceUpdate();
}
}

/** @inheritdoc */
render() {
return <this.Component { ...this.props } />;
}
Expand Down
139 changes: 133 additions & 6 deletions components/higher-order/with-filters/test/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';
import { mount, shallow } from 'enzyme';

/**
* WordPress dependencies
*/
import { addFilter, removeAllFilters } from '@wordpress/hooks';
import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks';

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

describe( 'withFilters', () => {
let wrapper;

const hookName = 'EnhancedComponent';
const MyComponent = () => <div>My component</div>;

afterEach( () => {
wrapper.unmount();
removeAllFilters( hookName );
} );

it( 'should display original component when no filters applied', () => {
const EnhancedComponent = withFilters( hookName )( MyComponent );
const wrapper = shallow( <EnhancedComponent /> );

wrapper = shallow( <EnhancedComponent /> );

expect( wrapper.html() ).toBe( '<div>My component</div>' );
} );
Expand All @@ -37,7 +41,7 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );

const wrapper = shallow( <EnhancedComponent /> );
wrapper = shallow( <EnhancedComponent /> );

expect( wrapper.html() ).toBe( '<div>Overridden component</div>' );
} );
Expand All @@ -56,8 +60,131 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );

const wrapper = shallow( <EnhancedComponent /> );
wrapper = shallow( <EnhancedComponent /> );

expect( wrapper.html() ).toBe( '<div><div>My component</div><div>Composed component</div></div>' );
} );

it( 'should re-render component once when new filter added after component was mounted', () => {
const spy = jest.fn();
const SpiedComponent = () => {
spy();
return <div>Spied component</div>;
};
const EnhancedComponent = withFilters( hookName )( SpiedComponent );

wrapper = mount( <EnhancedComponent /> );

spy.mockClear();
addFilter(
hookName,
'test/enhanced-component-spy-1',
FilteredComponent => () => (
<blockquote>
<FilteredComponent />
</blockquote>
),
);
jest.runAllTimers();

expect( spy ).toHaveBeenCalledTimes( 1 );
expect( wrapper.html() ).toBe( '<blockquote><div>Spied component</div></blockquote>' );
} );

it( 'should re-render component once when two filters added in the same animation frame', () => {
const spy = jest.fn();
const SpiedComponent = () => {
spy();
return <div>Spied component</div>;
};
const EnhancedComponent = withFilters( hookName )( SpiedComponent );
wrapper = mount( <EnhancedComponent /> );

spy.mockClear();

addFilter(
hookName,
'test/enhanced-component-spy-1',
FilteredComponent => () => (
<blockquote>
<FilteredComponent />
</blockquote>
),
);
addFilter(
hookName,
'test/enhanced-component-spy-2',
FilteredComponent => () => (
<section>
<FilteredComponent />
</section>
),
);
jest.runAllTimers();

expect( spy ).toHaveBeenCalledTimes( 1 );
expect( wrapper.html() ).toBe( '<section><blockquote><div>Spied component</div></blockquote></section>' );
} );

it( 'should re-render component twice when new filter added and removed in two different animation frames', () => {
const spy = jest.fn();
const SpiedComponent = () => {
spy();
return <div>Spied component</div>;
};
const EnhancedComponent = withFilters( hookName )( SpiedComponent );
wrapper = mount( <EnhancedComponent /> );

spy.mockClear();
addFilter(
hookName,
'test/enhanced-component-spy',
FilteredComponent => () => (
<div>
<FilteredComponent />
</div>
),
);
jest.runAllTimers();

removeFilter(
hookName,
'test/enhanced-component-spy',
);
jest.runAllTimers();

expect( spy ).toHaveBeenCalledTimes( 2 );
expect( wrapper.html() ).toBe( '<div>Spied component</div>' );
} );

it( 'should re-render both components once each when one filter added', () => {
const spy = jest.fn();
const SpiedComponent = () => {
spy();
return <div>Spied component</div>;
};
const EnhancedComponent = withFilters( hookName )( SpiedComponent );
const CombinedComponents = () => (
<div>
<EnhancedComponent />
<EnhancedComponent />
</div>
);
wrapper = mount( <CombinedComponents /> );

spy.mockClear();
addFilter(
hookName,
'test/enhanced-component-spy-1',
FilteredComponent => () => (
<blockquote>
<FilteredComponent />
</blockquote>
),
);
jest.runAllTimers();

expect( spy ).toHaveBeenCalledTimes( 2 );
expect( wrapper.html() ).toBe( '<div><blockquote><div>Spied component</div></blockquote><blockquote><div>Spied component</div></blockquote></div>' );
} );
} );

0 comments on commit e9f38ae

Please sign in to comment.