diff --git a/.eslintrc b/.eslintrc index 05098a4..f63386d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,7 @@ // Semi-strict max width "max-len": [ - "error", + 1, { "ignoreComments": true, "ignoreStrings": true, diff --git a/README.md b/README.md index 501378a..04fb294 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,28 @@ [![Travis](https://img.shields.io/travis/ctrlplusb/react-sizeme.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-sizeme) [![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-sizeme.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-sizeme) +```javascript +import sizeMe from 'react-sizeme' + +function MyComponent({ size }) { + return ( +
My width is {size.width}px
+ ) +} + +export default sizeMe()(MyComponent) +``` + * Responsive Components! * Easy to use. * Extensive browser support. * Supports any Component type, i.e. stateless/class. -* Works with React 0.14.x and 15.x.x. * 7.67KB gzipped standalone, even smaller if bundled with your assets. ## TOCs - - [What is this for?](https://github.com/ctrlplusb/react-sizeme#what-is-this-for) - - [Release Notes](https://github.com/ctrlplusb/react-sizeme#release-notes) - - [Live Demo](https://github.com/ctrlplusb/react-sizeme#live-demo) + - [Intro](https://github.com/ctrlplusb/react-sizeme#intro) + - [Demo](https://github.com/ctrlplusb/react-sizeme#live-demo) - [Quick Example](https://github.com/ctrlplusb/react-sizeme#quick-example) - [Usage and API Details](https://github.com/ctrlplusb/react-sizeme#usage-and-api-details) - [`react-component-queries`: a highly recommended abstraction](https://github.com/ctrlplusb/react-sizeme#react-component-queries-a-highly-recommended-abstraction) @@ -29,41 +39,16 @@ - [Extreme Appreciation](https://github.com/ctrlplusb/react-sizeme#extreme-appreciation) -## What is this for? +## Intro -Give your Components the ability to have render logic based on their height/width. Responsive design on the Component level. This allows you to create highly reusable components that don't care about where they will be rendered. +Give your Components the ability to have render logic based on their height/width/position. Responsive design on the Component level. This allows you to create highly reusable components that don't care about where they will be rendered. -## Live Demo +## Demo It really does work! Look: https://react-sizeme-example-anpinwkzyc.now.sh -## Release Notes - -See here: https://github.com/ctrlplusb/react-sizeme/releases - -## Quick Example - -Below is a super simple example highlighting the use of the library. Read the Usage section in its entirety for a full description on configuration and usage. - -```javascript -import sizeMe from 'react-sizeme'; - -class MyComponent extends Component { - render() { - // We receive a "size" prop that contains "width" and "height"! - // Note: they may be null until the first measure has taken place. - return ( -
My width is {this.props.size.width}px
- ); - } -} - -// Wired up here! -export default sizeMe()(MyComponent); -``` - ## Usage and API Details First install the library. @@ -80,33 +65,29 @@ import sizeMe from 'react-sizeme'; When using the `sizeMe` function you first have to pass it a configuration object. The entire configuration object is optional, as is each of its properties (in which case the defaults would be used). -Here is a full specification of all the properties available to the configuration object: +Here is a full specification of all the properties available to the configuration object, with the default values assigned: ```javascript const sizeMeConfig = { - // If true, then any changes to your Components rendered width will cause an // recalculation of the "size" prop which will then be be passed into // your Component. - // If false, then any changes to your Components rendered width will NOT - // cause any recalculation of the "size" prop. Additionally any "size" prop - // that is passed into your Component will always have a `null` value - // for the "width" property. - monitorWidth: true, // Default value + monitorWidth: true, // If true, then any changes to your Components rendered height will cause an // recalculation of the "size" prop which will then be be passed into // your Component. - // If false, then any changes to your Components rendered height will NOT - // cause any recalculation of the "size" prop. Additionally any "size" prop - // that is passed into your Component will always have a `null` value - // for the "height" property. - monitorHeight: false, // Default value + monitorHeight: false, + + // If true, then any changes to your Components position will cause an + // recalculation of the "size" prop which will then be be passed into + // your Component. + monitorPosition: false, // The maximum frequency, in milliseconds, at which size changes should be // recalculated when changes in your Component's rendered size are being // detected. This should not be set to lower than 16. - refreshRate: 16, // Default value + refreshRate: 16, // The mode in which refreshing should occur. Valid values are "debounce" // and "throttle". "throttle" will eagerly measure your component and then @@ -114,11 +95,11 @@ const sizeMeConfig = { // changes. "debounce" will wait for a minimum of the refreshRate before // it does a measurement check on your component. "debounce" can be useful // in cases where your component is animated into the DOM. - refreshMode: 'throttle' // Default value -}; + refreshMode: 'throttle' +} ``` -When you execute the `SizeMe` function it will return a Higher Order Component (HOC). You can use this Higher Order Component to decorate any of your existing Components with the size awareness ability. Each of the Components you decorate will then recieve a `size` prop, which is an object of schema `{ width: ?number, height: ?number }` - the numbers representing pixel values. Note that the values can be null until the first measurement has taken place, or based on your configuration. Here is a verbose example showing full usage of the API: +When you execute the `sizeMe` function it will return a Higher Order Component (HOC). You can use this Higher Order Component to decorate any of your existing Components with the size awareness ability. Each of the Components you decorate will then recieve a `size` prop, which is an object of schema `{ width: ?number, height: ?number, position: ?{ left: number, top: number, right: number, bottom: number} }` - the numbers representing pixel values. Note that the values can be null until the first measurement has taken place, or based on your configuration. Here is a verbose example showing full usage of the API: ```javascript import sizeMe from 'react-sizeme'; @@ -197,8 +178,8 @@ export default sizeMe({ monitorHeight: true })(MyComponent); __IMPORTANT__: -* We don't monitor height by default, so if you use the default settings and your component only changes in height it won't cause a recalculation of the `size` prop. I figured that in most cases we care about the width only and it would be annoying if vertical text spanning kept throwing out updates. -* If you aren't monitoring a specific dimension (width or height) you will be provided `null` values for the respective dimension. This is to avoid any possible misconfigured implementation whoopsies. In the case of Server Side Rendering you would also receive nulls - read more about the SSR case [here](https://github.com/ctrlplusb/react-sizeme#server-side-rendering). +* We don't monitor height or position by default as these are likely to create a high throughput of "size prop updates". It is up to you to enable and handle these appropriately. +* If you aren't monitoring a specific dimension (width, height, position) you will be provided `null` values for the respective dimension. This is to avoid any possible misconfigured implementation whoopsies. In the case of Server Side Rendering you would also receive nulls - read more about the SSR case [here](https://github.com/ctrlplusb/react-sizeme#server-side-rendering). * `refreshRate` is set very low. If you are using this library in a manner where you expect loads of active changes to your components dimensions you may need to tweak this value to avoid browser spamming. ## `react-component-queries`: a highly recommended abstraction @@ -267,9 +248,9 @@ If however you wish to use this library to wrap a component that you expect to b ## Server Side Rendering -Okay, I am gonna be up front here and tell you that using this library in an SSR context is most likely a bad idea. However, if you insist on doing so you then you should take the time to make yourself fully aware of any possible repurcussions you application may face. +Okay, I am gonna be up front here and tell you that using this library in an SSR context is most likely a bad idea. If you insist on doing so you then you should take the time to make yourself fully aware of any possible repercussions you application may face. -A standard `sizeMe` configuration involves the rendering of a placeholder component. After the placeholder is mounted to the DOM we extract it's dimension information and pass it on to your actual component. We do this in order to avoid any unneccesary render cycles for possibly deep component trees. Whilst this is useful for a purely client side set up, this is less than useful for an SSR context as the delivered page will contain empty placeholders. Ideally you want actual content to be delivered so that users without JS can still have an experience, or SEO bots can scrape your website. +A standard `sizeMe` configuration involves the rendering of a placeholder component. After the placeholder is mounted to the DOM we extract it's dimension information and pass it on to your actual component. We do this in order to avoid any unnecessary render cycles for possibly deep component trees. Whilst this is useful for a purely client side set up, this is less than useful for an SSR context as the delivered page will contain empty placeholders. Ideally you want actual content to be delivered so that users without JS can still have an experience, or SEO bots can scrape your website. Therefore we have provided a global configuration flag on `SizeMe`. Setting this flag will switch the library into an SSR mode, which essentially disables any placeholder rendering. Instead your wrapped component will be rendered directly. You should set the flag within the initialisation of your application (for both client/server). diff --git a/src/__tests__/sizeMe.test.js b/src/__tests__/sizeMe.test.js index 1bf66fc..6121cae 100644 --- a/src/__tests__/sizeMe.test.js +++ b/src/__tests__/sizeMe.test.js @@ -5,7 +5,7 @@ import React from 'react' import sinon from 'sinon' import { mount } from 'enzyme' -import { renderToString } from 'react-dom/server' +import { renderToStaticMarkup } from 'react-dom/server' describe('Given the SizeMe library', () => { let sizeMe @@ -13,6 +13,45 @@ describe('Given the SizeMe library', () => { let resizeDetectorMock const placeholderHtml = '
' + const SizeRender = ({ size = {}, debug }) => { + const { width, height, position } = size + const p = position || {} + const result = ( +
+ w: {width || 'null'}, + h: {height || 'null'}, + l: {p.left || 'null'}, + r: {p.right || 'null'}, + t: {p.top || 'null'}, + b: {p.bottom || 'null'} +
+ ) + if (debug) { + console.log(result) + } + return result + } + + const expected = ({ width, height, position }) => { + const p = position || {} + return `w: ${width || 'null'}, h: ${height || 'null'}, l: ${p.left || 'null'}, r: ${p.right || 'null'}, t: ${p.top || 'null'}, b: ${p.bottom || 'null'}` + } + + const delay = (fn, time) => + new Promise((resolve, reject) => { + setTimeout( + () => { + try { + fn() + } catch (err) { + reject(err) + } + resolve() + }, + time, + ) + }) + beforeEach(() => { sizeMe = require('../sizeMe').default @@ -48,7 +87,7 @@ describe('Given the SizeMe library', () => { const action = () => sizeMe({ monitorHeight: false, monitorWidth: false }) expect(action).toThrow( - /You have to monitor at least one of the width or height/, + /You have to monitor at least one of the width, height, or position/, ) }) }) @@ -56,7 +95,7 @@ describe('Given the SizeMe library', () => { describe('When mounting and unmounting the placeholder component', () => { it('Then the resizeDetector registration and deregistration should be called', () => { - const SizeAwareComponent = sizeMe()(() =>
) + const SizeAwareComponent = sizeMe()(SizeRender) const mounted = mount() @@ -71,44 +110,43 @@ describe('Given the SizeMe library', () => { }) describe('When setting the "debounce" refreshMode', () => { - it('Then the size data should only appear after the refresh rate has expired', (done) => { + it('Then the size data should only appear after the refresh rate has expired', () => { const config = { refreshMode: 'debounce', refreshRate: 50, monitorHeight: true, } - const SizeAwareComponent = sizeMe(config)(({ - size: { width, height }, - }) =>
{width} x {height}
) + const SizeAwareComponent = sizeMe(config)(SizeRender) const mounted = mount() // Get the callback for size changes. - const checkIfSizeChangedCallback = resizeDetectorMock.listenTo.args[0][1] + const { listenTo } = resizeDetectorMock + const checkIfSizeChangedCallback = listenTo.args[0][1] checkIfSizeChangedCallback({ getBoundingClientRect: () => ({ width: 100, - height: 100, + height: 50, }), }) - setTimeout(() => expect(mounted.text()).toEqual(''), 25) - setTimeout( - () => { - expect(mounted.text()).toEqual('100 x 100') - done() - }, - 55, - ) + return Promise.all([ + delay(() => expect(mounted.text()).toEqual(''), 25), + delay( + () => + expect(mounted.text()).toEqual( + expected({ width: 100, height: 50 }), + ), + 60, + ), + ]) }) }) describe('When the wrapped component gets mounted after the placeholder', () => { it('Then the resizeDetector registration and deregistration should be called', () => { const config = { monitorHeight: true } - const SizeAwareComponent = sizeMe(config)(({ - size: { width, height }, - }) =>
{width} x {height}
) + const SizeAwareComponent = sizeMe(config)(SizeRender) const mounted = mount() @@ -121,14 +159,14 @@ describe('Given the SizeMe library', () => { checkIfSizeChangedCallback({ getBoundingClientRect: () => ({ width: 100, - height: 100, + height: 50, }), }) // Our actual component should have mounted, therefore a removelistener // should have been called on the placeholder, and an add listener // on the newly mounted component. - expect(mounted.text()).toEqual('100 x 100') + expect(mounted.text()).toEqual(expected({ width: 100, height: 50 })) expect(resizeDetectorMock.listenTo.callCount).toEqual(2) expect(resizeDetectorMock.removeAllListeners.callCount).toEqual(1) @@ -143,42 +181,34 @@ describe('Given the SizeMe library', () => { describe('When no className or style has been provided', () => { it('Then it should render the default placeholder', () => { - const SizeAwareComponent = sizeMe()(() =>
) - + const SizeAwareComponent = sizeMe()(SizeRender) const mounted = mount() - expect(mounted.html()).toEqual(placeholderHtml) }) }) describe('When only a className has been provided', () => { it('Then it should render a placeholder with the className', () => { - const SizeAwareComponent = sizeMe()(() =>
) - + const SizeAwareComponent = sizeMe()(SizeRender) const mounted = mount() - expect(mounted.html()).toEqual('
') }) }) describe('When only a style has been provided', () => { it('Then it should render a placeholder with the style', () => { - const SizeAwareComponent = sizeMe()(() =>
) - + const SizeAwareComponent = sizeMe()(SizeRender) const mounted = mount() - expect(mounted.html()).toEqual('
') }) }) describe('When a className and style have been provided', () => { it('Then it should render a placeholder with both', () => { - const SizeAwareComponent = sizeMe()(() =>
) - + const SizeAwareComponent = sizeMe()(SizeRender) const mounted = mount( , ) - expect(mounted.html()).toEqual( '
', ) @@ -190,9 +220,7 @@ describe('Given the SizeMe library', () => { const SizeAwareComponent = sizeMe({ monitorWidth: true, monitorHeight: false, - })(({ size: { width, height } }) => ( -
{width} x {height || 'null'}
- )) + })(SizeRender) const mounted = mount() @@ -206,7 +234,7 @@ describe('Given the SizeMe library', () => { }) // Update should have occurred immediately. - expect(mounted.text()).toEqual('100 x null') + expect(mounted.text()).toEqual(expected({ width: 100 })) }) }) @@ -215,9 +243,7 @@ describe('Given the SizeMe library', () => { const SizeAwareComponent = sizeMe({ monitorWidth: false, monitorHeight: true, - })(({ size: { width, height } }) => ( -
{width || 'null'} x {height}
- )) + })(SizeRender) const mounted = mount() @@ -231,7 +257,40 @@ describe('Given the SizeMe library', () => { }) // Update should have occurred immediately. - expect(mounted.text()).toEqual('null x 150') + expect(mounted.text()).toEqual(expected({ height: 150 })) + }) + }) + + describe('When the size event has occurred when only position is being monitored', () => { + it('Then expected position should be provided to the rendered component', () => { + const SizeAwareComponent = sizeMe({ + monitorWidth: false, + monitorHeight: false, + monitorPosition: true, + })(SizeRender) + + const mounted = mount() + + // Initial render should be as expected. + expect(mounted.html()).toEqual(placeholderHtml) + + // Get the callback for size changes. + const checkIfSizeChangedCallback = resizeDetectorMock.listenTo.args[0][1] + checkIfSizeChangedCallback({ + getBoundingClientRect: () => ({ + width: 100, + height: 150, + left: 55, + right: 66, + top: 77, + bottom: 88, + }), + }) + + // Update should have occurred immediately. + expect(mounted.text()).toEqual( + expected({ position: { left: 55, right: 66, top: 77, bottom: 88 } }), + ) }) }) @@ -240,7 +299,7 @@ describe('Given the SizeMe library', () => { const SizeAwareComponent = sizeMe({ monitorWidth: true, monitorHeight: true, - })(({ size: { width, height } }) =>
{width} x {height}
) + })(SizeRender) const mounted = mount() @@ -254,7 +313,7 @@ describe('Given the SizeMe library', () => { }) // Update should have occurred immediately. - expect(mounted.text()).toEqual('100 x 150') + expect(mounted.text()).toEqual(expected({ height: 150, width: 100 })) }) }) @@ -263,26 +322,27 @@ describe('Given the SizeMe library', () => { const SizeAwareComponent = sizeMe({ monitorHeight: true, monitorWidth: true, - })(({ size: { width, height }, otherProp }) => ( -
{width} x {height} & {otherProp}
- )) + monitorPos: true, + })(({ otherProp }) =>
{otherProp}
) const mounted = mount() // Get the callback for size changes. const checkIfSizeChangedCallback = resizeDetectorMock.listenTo.args[0][1] checkIfSizeChangedCallback({ - getBoundingClientRect: () => ({ width: 100, height: 100 }), + getBoundingClientRect: () => ({ + width: 100, + height: 100, + left: 55, + right: 66, + top: 77, + bottom: 88, + }), }) - // Output should contain foo. - expect(mounted.text()).toEqual('100 x 100 & foo') - - // Update the other prop. + expect(mounted.text()).toEqual('foo') mounted.setProps({ otherProp: 'bar' }) - - // Output should contain foo. - expect(mounted.text()).toEqual('100 x 100 & bar') + expect(mounted.text()).toEqual('bar') }) }) @@ -295,16 +355,13 @@ describe('Given the SizeMe library', () => { const SizeAwareComponent = sizeMe({ monitorHeight: true, monitorWidth: true, - })(({ size: { width, height } }) => ( -
{width || 'undefined'} x {height || 'undefined'}
- )) + })(SizeRender) - const mounted = renderToString( + const actual = renderToStaticMarkup( , - ).replace(//g, '') + ) - // Output should contain undefined for width and height. - expect(mounted).toContain('undefined x undefined') + expect(actual).toContain(expected({})) }) }) }) diff --git a/src/sizeMe.js b/src/sizeMe.js index e1a4186..581825e 100644 --- a/src/sizeMe.js +++ b/src/sizeMe.js @@ -9,6 +9,7 @@ import resizeDetector from './resizeDetector' const defaultConfig = { monitorWidth: true, monitorHeight: false, + monitorPosition: false, refreshRate: 16, refreshMode: 'throttle', } @@ -73,11 +74,14 @@ const renderWrapper = (WrappedComponent) => { disablePlaceholder, ...restProps } = props - const { width, height } = size + const { width, height, position } = size - const toRender = width === undefined && + const renderPlaceholder = width === undefined && height === undefined && + position === undefined && !disablePlaceholder + + const toRender = renderPlaceholder ? : ( { - const { height: cHeight, width: cWidth } = current - const { height: nHeight, width: nWidth } = next - - return (monitorHeight && cHeight !== nHeight) || - (monitorWidth && cWidth !== nWidth) + const c = current + const n = next + const cp = c.position || {} + const np = n.position || {} + + return (monitorHeight && c.height !== n.height) || + (monitorPosition && + (cp.top !== np.top || + cp.left !== np.left || + cp.bottom !== np.bottom || + cp.right !== np.right)) || + (monitorWidth && c.width !== n.width) }; checkIfSizeChanged = refreshDelayStrategy( (el) => { - const { width, height } = el.getBoundingClientRect() + const { + width, + height, + right, + left, + top, + bottom, + } = el.getBoundingClientRect() + const next = { width: monitorWidth ? width : null, height: monitorHeight ? height : null, + position: monitorPosition ? { right, left, top, bottom } : null, } if (this.hasSizeChanged(this.state, next)) { @@ -234,12 +254,12 @@ function sizeMe(config = defaultConfig) { ); render() { - const { width, height } = this.state + const { width, height, position } = this.state return (