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( '
Spied component
' ); + } ); + + 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",