diff --git a/.eslines.json b/.eslines.json
deleted file mode 100644
index 78b0ee74eb22e..0000000000000
--- a/.eslines.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "branches": {
- "default": ["downgrade-unmodified-lines"]
- },
- "processors": {
- "downgrade-unmodified-lines": {
- "remote": "origin/master",
- "rulesNotToDowngrade": ["no-unused-vars"]
- }
- }
-}
diff --git a/components/higher-order/with-filters/index.js b/components/higher-order/with-filters/index.js
index b301afb6622d0..883d683574dfa 100644
--- a/components/higher-order/with-filters/index.js
+++ b/components/higher-order/with-filters/index.js
@@ -1,13 +1,21 @@
+/**
+ * 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.
+ * 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.
*
@@ -16,11 +24,41 @@ import { applyFilters } from '@wordpress/hooks';
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
' );
+ } );
} );
diff --git a/docs/templates.md b/docs/templates.md
index 7dc32ff353237..c60c56f095bfd 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -74,11 +74,14 @@ Sometimes the intention might be to lock the template on the UI so that the bloc
It is also possible to assign a template to an existing post type like "posts" and "pages":
```php
-$post_type_object = get_post_type_object( 'post' );
-$post_type_object->template = array(
- array( 'core/paragraph', array(
- 'placeholder' => 'Add Description...',
- ) ),
-);
-$post_type_object->template_lock = 'all';
+function my_add_template_to_posts() {
+ $post_type_object = get_post_type_object( 'post' );
+ $post_type_object->template = array(
+ array( 'core/paragraph', array(
+ 'placeholder' => 'Add Description...',
+ ) ),
+ );
+ $post_type_object->template_lock = 'all';
+}
+add_action( 'init', 'my_add_template_to_posts' );
```
\ No newline at end of file
diff --git a/gutenberg.php b/gutenberg.php
index 3ca27ecfb26a0..9d5c7e4499bc4 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Gutenberg
* Plugin URI: https://github.com/WordPress/gutenberg
- * Description: Printing since 1440. This is the development plugin for the new block editor in core. Meant for development, do not run on real sites.
+ * Description: Printing since 1440. This is the development plugin for the new block editor in core.
* Version: 2.0.0
* Author: Gutenberg Team
*
diff --git a/package-lock.json b/package-lock.json
index 4ac3e090da921..60cdea8dafb07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3242,44 +3242,6 @@
"estraverse": "4.2.0"
}
},
- "eslines": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/eslines/-/eslines-1.1.0.tgz",
- "integrity": "sha1-eA3YIE5bluBb3I8BUCzVJ1q/xGQ=",
- "dev": true,
- "requires": {
- "jest-docblock": "20.0.3",
- "optionator": "0.8.1"
- },
- "dependencies": {
- "fast-levenshtein": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz",
- "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=",
- "dev": true
- },
- "jest-docblock": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz",
- "integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=",
- "dev": true
- },
- "optionator": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz",
- "integrity": "sha1-4xtJMs3V+4Yqiw0QvGPT7h7H14s=",
- "dev": true,
- "requires": {
- "deep-is": "0.1.3",
- "fast-levenshtein": "1.1.4",
- "levn": "0.3.0",
- "prelude-ls": "1.1.2",
- "type-check": "0.3.2",
- "wordwrap": "1.0.0"
- }
- }
- }
- },
"eslint": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.9.0.tgz",
diff --git a/package.json b/package.json
index b9cf9b395614c..9423b0abe8b2d 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,6 @@
"deep-freeze": "0.0.1",
"enzyme": "3.2.0",
"enzyme-adapter-react-16": "1.1.0",
- "eslines": "1.1.0",
"eslint": "4.9.0",
"eslint-config-wordpress": "2.0.0",
"eslint-plugin-jest": "21.5.0",
@@ -137,7 +136,7 @@
"prebuild": "check-node-version --package",
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"gettext-strings": "cross-env BABEL_ENV=gettext webpack",
- "lint": "eslint -f json . | eslines",
+ "lint": "eslint .",
"lint-php": "docker-compose run --rm composer run-script lint",
"predev": "check-node-version --package",
"dev": "cross-env BABEL_ENV=default webpack --watch",