diff --git a/components/higher-order/with-filters/index.js b/components/higher-order/with-filters/index.js
index 56ab88a7b75c7..8220c87933885 100644
--- a/components/higher-order/with-filters/index.js
+++ b/components/higher-order/with-filters/index.js
@@ -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 ;
}
diff --git a/components/higher-order/with-filters/test/index.js b/components/higher-order/with-filters/test/index.js
index c7931c43d593f..2c01d0ecf9883 100644
--- a/components/higher-order/with-filters/test/index.js
+++ b/components/higher-order/with-filters/test/index.js
@@ -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 = () =>
My component
;
afterEach( () => {
+ wrapper.unmount();
removeAllFilters( hookName );
} );
it( 'should display original component when no filters applied', () => {
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'My component
' );
} );
@@ -37,7 +41,7 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'Overridden component
' );
} );
@@ -56,8 +60,131 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'My component
Composed component
' );
} );
+
+ it( 'should re-render component once when new filter added after component was mounted', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 1 );
+ expect( wrapper.html() ).toBe( 'Spied component
' );
+ } );
+
+ it( 'should re-render component once when two filters added in the same animation frame', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ wrapper = mount( );
+
+ spy.mockClear();
+
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-2',
+ FilteredComponent => () => (
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 1 );
+ expect( wrapper.html() ).toBe( '' );
+ } );
+
+ 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 Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ removeFilter(
+ hookName,
+ 'test/enhanced-component-spy',
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 2 );
+ expect( wrapper.html() ).toBe( 'Spied component
' );
+ } );
+
+ it( 'should re-render both components once each when one filter added', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ const CombinedComponents = () => (
+
+
+
+
+ );
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 2 );
+ expect( wrapper.html() ).toBe( 'Spied component
Spied component
' );
+ } );
} );