├── .babelrc ├── .editorconfig ├── .eslintignore ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── logo.png ├── package.json ├── react-sizeme.d.ts ├── rollup-min.config.js ├── rollup.config.js ├── src ├── __tests__ │ ├── component.test.js │ ├── typescript.test.js │ ├── typescript │ │ ├── component.tsx │ │ ├── hoc.tsx │ │ └── tsconfig.json │ └── with-size.test.js ├── component.js ├── index.js ├── resize-detector.js └── with-size.js ├── tools ├── .eslintrc ├── scripts │ └── build.js └── utils.js ├── wallaby.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": true 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ], 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | commonjs/ 3 | coverage/ 4 | umd/ 5 | src/__tests__/typescript/ 6 | *.ts -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # ctrlplusb 5 | open_collective: controlplusb 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependencies 6 | node_modules 7 | 8 | # Debug log from npm 9 | npm-debug.log 10 | 11 | # Jest 12 | coverage 13 | 14 | # Build output 15 | dist 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | node_js: 8 | - '14' 9 | script: 10 | - npm run lint 11 | - npm run build 12 | after_success: 13 | # Deploy code coverage report to codecov.io 14 | - npm run test:coverage:deploy 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | We follow semantic versioning. 2 | 3 | See the [releases](https://github.com/ctrlplusb/react-sizeme/releases) page on 4 | GitHub for information regarding each release. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sean Matheson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

 

2 | 3 |

4 | 5 |

Make your React Components aware of their width and/or height!

6 |

7 | 8 |

 

9 | 10 | [![npm](https://img.shields.io/npm/v/react-sizeme.svg?style=flat-square)](http://npm.im/react-sizeme) 11 | [![MIT License](https://img.shields.io/npm/l/react-sizeme.svg?style=flat-square)](http://opensource.org/licenses/MIT) 12 | [![Travis](https://img.shields.io/travis/ctrlplusb/react-sizeme.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-sizeme) 13 | [![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-sizeme.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-sizeme) 14 | 15 | - Hyper Responsive Components! 16 | - Performant. 17 | - Easy to use. 18 | - Extensive browser support. 19 | - Supports functional and class Component types. 20 | - Tiny bundle size. 21 | - Demo: https://4mkpc.csb.app/ 22 | 23 | Use it via the render prop pattern (supports `children` or `render` prop): 24 | 25 | ```javascript 26 | import { SizeMe } from 'react-sizeme' 27 | 28 | function MyApp() { 29 | return {({ size }) =>
My width is {size.width}px
}
30 | } 31 | ``` 32 | 33 | Or, via a higher order component: 34 | 35 | ```javascript 36 | import { withSize } from 'react-sizeme' 37 | 38 | function MyComponent({ size }) { 39 | return
My width is {size.width}px
40 | } 41 | 42 | export default withSize()(MyComponent) 43 | ``` 44 | 45 |

 

46 | 47 | --- 48 | 49 | ## TOCs 50 | 51 | - [Intro](https://github.com/ctrlplusb/react-sizeme#intro) 52 | - [Installation](https://github.com/ctrlplusb/react-sizeme#installation) 53 | - [Configuration](https://github.com/ctrlplusb/react-sizeme#configuration) 54 | - [Component Usage](https://github.com/ctrlplusb/react-sizeme#component-usage) 55 | - [HOC Usage](https://github.com/ctrlplusb/react-sizeme#hoc-usage) 56 | - [`onSize` callback alternative usage](https://github.com/ctrlplusb/react-sizeme#onsize-callback-alternative-usage) 57 | - [Under the hood](https://github.com/ctrlplusb/react-sizeme#under-the-hood) 58 | - [Examples](#examples) 59 | - [Loading different child components based on size](#loading-different-child-components-based-on-size) 60 | - [Server Side Rendering](https://github.com/ctrlplusb/react-sizeme#server-side-rendering) 61 | - [Extreme Appreciation](https://github.com/ctrlplusb/react-sizeme#extreme-appreciation) 62 | - [Backers](https://github.com/ctrlplusb/react-sizeme#backers) 63 | 64 |

 

65 | 66 | --- 67 | 68 | ## Intro 69 | 70 | Give your Components the ability to have render logic based on their height and/or width. Responsive design on the Component level. This allows you to create highly reusable components that can adapt to wherever they are rendered. 71 | 72 | Check out a working demo here: https://4mkpc.csb.app/ 73 | 74 |

 

75 | 76 | --- 77 | 78 | ## Installation 79 | 80 | Firstly, ensure you have the required peer dependencies: 81 | 82 | ```bash 83 | npm install react react-dom 84 | ``` 85 | 86 | > **Note:** We require >=react@0.14.0 and >=react-dom@0.14.0 87 | 88 | ```bash 89 | npm install react-sizeme 90 | ``` 91 | 92 |

 

93 | 94 | --- 95 | 96 | ## Configuration 97 | 98 | The following configuration options are available. Please see the usage docs for how to pass these configuration values into either the [component](#component-usage) or [higher order function](#hoc-usage). 99 | 100 | - `monitorWidth` (_boolean_, **default**: true) 101 | 102 | 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. 103 | 104 | - `monitorHeight` (_boolean_, **default**: false) 105 | 106 | If true, then any changes to your Components rendered height will cause an 107 | recalculation of the "size" prop which will then be be passed into 108 | your Component. 109 | 110 | > PLEASE NOTE: that this is set to `false` by default 111 | 112 | - `refreshRate` (_number_, **default**: 16) 113 | 114 | 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. 115 | 116 | - `refreshMode` (_string_, **default**: 'throttle') 117 | 118 | The mode in which refreshing should occur. Valid values are "debounce" and "throttle". 119 | 120 | "throttle" will eagerly measure your component and then wait for the refreshRate to pass before doing a new measurement on size changes. 121 | 122 | "debounce" will wait for a minimum of the refreshRate before it does a measurement check on your component. 123 | 124 | "debounce" can be useful in cases where your component is animated into the DOM. 125 | 126 | > NOTE: When using "debounce" mode you may want to consider disabling the placeholder as this adds an extra delay in the rendering time of your component. 127 | 128 | - `noPlaceholder` (_boolean_, **default**: false) 129 | 130 | By default we render a "placeholder" component initially so we can try and "prefetch" the expected size for your component. This is to avoid any unnecessary deep tree renders. If you feel this is not an issue for your component case and you would like to get an eager render of 131 | your component then disable the placeholder using this config option. 132 | 133 | > NOTE: You can set this globally. See the docs on first render. 134 | 135 |

 

136 | 137 | --- 138 | 139 | ## Component Usage 140 | 141 | We provide a "render props pattern" based component. You can import it like so: 142 | 143 | ```javascript 144 | import { SizeMe } from 'react-sizeme' 145 | ``` 146 | 147 | You then provide it either a `render` or `children` prop containing a function/component that will receive a `size` prop (an object with `width` and `height` properties): 148 | 149 | ```javascript 150 | {({ size }) =>
My width is {size.width}px
}
151 | ``` 152 | 153 | _or_ 154 | 155 | ```javascript 156 |
My width is {size.width}px
} /> 157 | ``` 158 | 159 | To provide [configuration](#configuration) you simply add any customisation as props. For example: 160 | 161 | ```javascript 162 |
My width is {size.width}px
} 166 | /> 167 | ``` 168 | 169 |

 

170 | 171 | --- 172 | 173 | ## HOC Usage 174 | 175 | We provide you with a higher order component function called `withSize`. You can import it like so: 176 | 177 | ```javascript 178 | import { withSize } from 'react-sizeme' 179 | ``` 180 | 181 | Firstly, you have to call the `withSize` function, passing in an optional [configuration](#configuration) object should you wish to customise the behaviour: 182 | 183 | ```javascript 184 | const withSizeHOC = withSize() 185 | ``` 186 | 187 | You can then use the returned Higher Order Component to decorate any of your existing Components with the size awareness ability: 188 | 189 | ```javascript 190 | const SizeAwareComponent = withSizeHOC(MyComponent) 191 | ``` 192 | 193 | Your component will then receive a `size` prop (an object with `width` and `height` properties). 194 | 195 | > Note that the values could be undefined based on the configuration you provided (e.g. you explicitly do not monitor either of the dimensions) 196 | 197 | Below is a full example: 198 | 199 | ```javascript 200 | import { withSize } from 'react-sizeme' 201 | 202 | class MyComponent extends Component { 203 | render() { 204 | const { width, height } = this.props.size 205 | 206 | return ( 207 |
208 | My size is {width || -1}px x {height || -1}px 209 |
210 | ) 211 | } 212 | } 213 | 214 | export default withSize({ monitorHeight: true })(MyComponent) 215 | ``` 216 | 217 | ### `onSize` callback alternative usage 218 | 219 | The higher order component also allows an alternative usage where you provide an `onSize` callback function. 220 | 221 | This allows the "parent" to manage the `size` value rather than your component, which can be useful in specific circumstances. 222 | 223 | Below is an example of it's usage. 224 | 225 | Firstly, create a component you wish to know the size of: 226 | 227 | ```jsx 228 | import { withSize } from 'react-sizeme' 229 | 230 | function MyComponent({ message }) { 231 | return
{message}
232 | } 233 | 234 | export default withSize()(MyComponent) 235 | ``` 236 | 237 | Now create a "parent" component providing it a `onSize` callback function to the size aware component: 238 | 239 | ```jsx 240 | class ParentComponent extends React.Component { 241 | onSize = (size) => { 242 | console.log('MyComponent has a width of', size.width) 243 | } 244 | 245 | render() { 246 | return 247 | } 248 | } 249 | ``` 250 | 251 |

 

252 | 253 | --- 254 | 255 | ## Under the hood 256 | 257 | It can be useful to understand the rendering workflow should you wish to debug any issues we may be having. 258 | 259 | In order to size your component we have a bit of a chicken/egg scenario. We can't know the width/height of your Component until it is rendered. This can lead wasteful rendering cycles should you choose to render your components based on their width/height. 260 | 261 | Therefore for the first render of your component we actually render a lightweight placeholder in place of your component in order to obtain the width/height. If your component was being passed a `className` or `style` prop then these will be applied to the placeholder so that it can more closely resemble your actual components dimensions. 262 | 263 | So the first dimensions that are passed to your component may not be "correct" dimensions, however, it should quickly receive the "correct" dimensions upon render. 264 | 265 | Should you wish to avoid the render of a placeholder and have an eager render of your component then you can use the `noPlaceholder` configuration option. Using this configuration value your component will be rendered directly, however, the `size` prop may contain `undefined` for width and height until your component completes its first render. 266 | 267 |

 

268 | 269 | --- 270 | 271 | ## Examples 272 | 273 | ### Loading different child components based on size 274 | 275 | ```javascript 276 | import React from 'react' 277 | import LargeChildComponent from './LargeChildComponent' 278 | import SmallChildComponent from './SmallChildComponent' 279 | import sizeMe from 'react-sizeme' 280 | 281 | function MyComponent(props) { 282 | const { width, height } = props.size 283 | 284 | const ToRenderChild = height > 600 ? LargeChildComponent : SmallChildComponent 285 | 286 | return ( 287 |
288 |

289 | My size is {width}x{height} 290 |

291 | 292 | 293 | ) 294 | } 295 | 296 | export default sizeMe({ monitorHeight: true })(MyComponent) 297 | ``` 298 | 299 | > EXTRA POINTS! Combine the above with a code splitting API (e.g. Webpack's System.import) to avoid unnecessary code downloads for your clients. Zing! 300 | 301 |

 

302 | 303 | --- 304 | 305 | ## Server Side Rendering 306 | 307 | 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. 308 | 309 | 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. 310 | 311 | To avoid the rendering of placeholders in this context you can make use of the `noPlaceholders` global configuration value. Setting this flag will disables any placeholder rendering. Instead your wrapped component will be rendered directly - however it's initial render will contain no values within the `size` prop (i.e. `width`, and `height` will be `null`). 312 | 313 | ```javascript 314 | import sizeMe from 'react-sizeme' 315 | 316 | // This is a global variable. i.e. will be the default for all instances. 317 | sizeMe.noPlaceholders = true 318 | ``` 319 | 320 | > Note: if you only partialy server render your application you may want to use the component level configuration that allows disabling placeholders per component (e.g. `sizeMe({ noPlaceholder: true })`) 321 | 322 | It is up to you to decide how you would like to initially render your component then. When your component is sent to the client and mounted to the DOM `SizeMe` will calculate and send the dimensions to your component as normal. I suggest you tread very carefully with how you use this updated information and do lots of testing using various screen dimensions. Try your best to avoid unnecessary re-rendering of your components, for the sake of your users. 323 | 324 | If you come up with any clever strategies for this please do come share them with us! :) 325 | 326 |

 

327 | 328 | --- 329 | 330 | ## Extreme Appreciation! 331 | 332 | We make use of the awesome [element-resize-detector](https://github.com/wnr/element-resize-detector) library. This library makes use of an scroll/object based event strategy which outperforms window resize event listening dramatically. The original idea for this approach comes from another library, namely [css-element-queries](https://github.com/marcj/css-element-queries) by Marc J. Schmidt. I recommend looking into these libraries for history, specifics, and more examples. I love them for the work they did, whithout which this library would not be possible. :sparkling_heart: 333 | 334 |

 

335 | 336 | --- 337 | 338 | ## Backers 339 | 340 | Thank goes to all our backers! [[Become a backer](https://opencollective.com/controlplusb#backer)]. 341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/react-sizeme/90423fce958ed133f9c85c03ba87fb25781e62a4/assets/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sizeme", 3 | "version": "3.0.2", 4 | "description": "Make your React Components aware of their width and/or height!", 5 | "license": "MIT", 6 | "main": "dist/react-sizeme.js", 7 | "types": "react-sizeme.d.ts", 8 | "files": [ 9 | "*.js", 10 | "*.md", 11 | "dist", 12 | "react-sizeme.d.ts" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ctrlplusb/react-sizeme.git" 17 | }, 18 | "keywords": [ 19 | "library" 20 | ], 21 | "homepage": "https://github.com/ctrlplusb/react-sizeme#readme", 22 | "author": "Sean Matheson ", 23 | "scripts": { 24 | "build": "node ./tools/scripts/build.js", 25 | "clean": "rimraf ./dist && rimraf ./coverage", 26 | "lint": "eslint src", 27 | "prepublish": "yarn run build", 28 | "test": "jest", 29 | "test:coverage": "yarn run test -- --coverage", 30 | "test:coverage:deploy": "yarn run test:coverage && codecov" 31 | }, 32 | "dependencies": { 33 | "element-resize-detector": "^1.2.2", 34 | "invariant": "^2.2.4", 35 | "shallowequal": "^1.1.0", 36 | "throttle-debounce": "^3.0.1" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.14.8", 40 | "@babel/core": "^7.15.0", 41 | "@babel/plugin-proposal-class-properties": "^7.14.5", 42 | "@babel/polyfill": "^7.12.1", 43 | "@babel/preset-env": "^7.15.0", 44 | "@babel/preset-react": "^7.14.5", 45 | "@babel/register": "^7.15.3", 46 | "@types/react": "^17.0.19", 47 | "@types/react-dom": "^17.0.9", 48 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3", 49 | "app-root-dir": "1.0.2", 50 | "babel-eslint": "^10.1.0", 51 | "babel-jest": "^26.6.3", 52 | "codecov": "^3.8.3", 53 | "cross-env": "^7.0.3", 54 | "enzyme": "^3.11.0", 55 | "enzyme-to-json": "^3.6.2", 56 | "eslint": "^7.32.0", 57 | "eslint-config-airbnb": "^18.2.1", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-plugin-import": "^2.24.1", 60 | "eslint-plugin-jsx-a11y": "^6.4.1", 61 | "eslint-plugin-react": "^7.24.0", 62 | "eslint-plugin-react-hooks": "^4.2.0", 63 | "gzip-size": "^6.0.0", 64 | "husky": "^4.3.8", 65 | "in-publish": "^2.0.1", 66 | "jest": "^26.6.3", 67 | "lint-staged": "^10.5.4", 68 | "prettier": "^2.3.2", 69 | "pretty-bytes": "5.6.0", 70 | "ramda": "^0.27.1", 71 | "react": "^17.0.2", 72 | "react-addons-test-utils": "^15.6.0", 73 | "react-dom": "^17.0.2", 74 | "react-test-renderer": "^17.0.2", 75 | "readline-sync": "1.4.10", 76 | "rimraf": "^3.0.2", 77 | "rollup": "^2.56.3", 78 | "rollup-plugin-babel": "^4.4.0", 79 | "rollup-plugin-uglify": "^6.0.4", 80 | "title-case": "^3.0.3", 81 | "typescript": "^4.3.5", 82 | "typings-tester": "^0.3.2" 83 | }, 84 | "jest": { 85 | "collectCoverageFrom": [ 86 | "src/**/*.{js,jsx}" 87 | ], 88 | "snapshotSerializers": [ 89 | "/node_modules/enzyme-to-json/serializer" 90 | ], 91 | "testPathIgnorePatterns": [ 92 | "/(coverage|dist|node_modules|tools)/", 93 | "/src/__tests__/typescript/" 94 | ], 95 | "testURL": "http://localhost/" 96 | }, 97 | "eslintConfig": { 98 | "root": true, 99 | "parser": "babel-eslint", 100 | "env": { 101 | "browser": true, 102 | "es6": true, 103 | "node": true, 104 | "jest": true 105 | }, 106 | "extends": [ 107 | "airbnb", 108 | "prettier" 109 | ], 110 | "rules": { 111 | "camelcase": 0, 112 | "import/prefer-default-export": 0, 113 | "import/no-extraneous-dependencies": 0, 114 | "max-classes-per-file": 0, 115 | "no-nested-ternary": 0, 116 | "no-underscore-dangle": 0, 117 | "react/no-array-index-key": 0, 118 | "react/react-in-jsx-scope": 0, 119 | "semi": [ 120 | 2, 121 | "never" 122 | ], 123 | "react/destructuring-assignment": 0, 124 | "react/forbid-prop-types": 0, 125 | "react/jsx-filename-extension": 0, 126 | "react/jsx-props-no-spreading": 0, 127 | "react/sort-comp": 0, 128 | "react/state-in-constructor": 0, 129 | "react/static-property-placement": 0 130 | } 131 | }, 132 | "eslintIgnore": [ 133 | "node_modules/", 134 | "dist/", 135 | "coverage/" 136 | ], 137 | "prettier": { 138 | "semi": false, 139 | "singleQuote": true, 140 | "trailingComma": "all" 141 | }, 142 | "lint-staged": { 143 | "*.js": [ 144 | "prettier --write \"src/**/*.js\"" 145 | ] 146 | }, 147 | "husky": { 148 | "hooks": { 149 | "pre-commit": "lint-staged && yarn run test" 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /react-sizeme.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { Component, ComponentType, ReactNode, ReactElement } from 'react' 4 | 5 | type Omit = Pick> 6 | 7 | declare namespace sizeMe { 8 | export interface SizeMeProps { 9 | readonly size: { 10 | readonly width: number | null 11 | readonly height: number | null 12 | } 13 | } 14 | 15 | export interface SizeMeOptions { 16 | monitorHeight?: boolean 17 | monitorWidth?: boolean 18 | noPlaceholder?: boolean 19 | refreshMode?: 'throttle' | 'debounce' 20 | refreshRate?: number 21 | resizeDetectorStrategy?: 'scroll' | 'object' 22 | } 23 | 24 | export interface SizeMeRenderProps extends SizeMeOptions { 25 | children: (props: SizeMeProps) => ReactElement 26 | } 27 | 28 | export class SizeMe extends Component {} 29 | 30 | export type WithSizeOnSizeCallback = (size: SizeMeProps['size']) => void 31 | 32 | export interface WithSizeProps { 33 | onSize?: WithSizeOnSizeCallback 34 | } 35 | 36 | export function withSize( 37 | options?: SizeMeOptions, 38 | ):

( 39 | component: ComponentType

, 40 | ) => ComponentType & WithSizeProps> 41 | 42 | export let noPlaceholders: boolean 43 | } 44 | 45 | declare function sizeMe( 46 | options?: sizeMe.SizeMeOptions, 47 | ):

( 48 | component: ComponentType

, 49 | ) => ComponentType & sizeMe.WithSizeProps> 50 | 51 | export = sizeMe 52 | -------------------------------------------------------------------------------- /rollup-min.config.js: -------------------------------------------------------------------------------- 1 | const { uglify } = require('rollup-plugin-uglify') 2 | const packageJson = require('./package.json') 3 | 4 | const baseConfig = require('./rollup.config.js') 5 | 6 | baseConfig.plugins.push(uglify()) 7 | baseConfig.output.file = `dist/${packageJson.name}.min.js` 8 | 9 | module.exports = baseConfig 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel') 2 | const { titleCase } = require('title-case') 3 | const packageJson = require('./package.json') 4 | 5 | process.env.BABEL_ENV = 'production' 6 | 7 | module.exports = { 8 | external: [ 9 | 'element-resize-detector', 10 | 'invariant', 11 | 'throttle-debounce', 12 | 'prop-types', 13 | 'react-dom', 14 | 'react', 15 | 'shallowequal', 16 | ], 17 | input: 'src/index.js', 18 | output: { 19 | file: `dist/${packageJson.name}.js`, 20 | format: 'cjs', 21 | sourcemap: true, 22 | name: titleCase(packageJson.name.replace(/-/g, ' ')).replace(/ /g, ''), 23 | exports: 'auto', 24 | }, 25 | plugins: [ 26 | babel({ 27 | babelrc: false, 28 | exclude: 'node_modules/**', 29 | presets: [ 30 | ['@babel/preset-env', { modules: false }], 31 | '@babel/preset-react', 32 | ], 33 | plugins: ['@babel/plugin-proposal-class-properties'], 34 | }), 35 | ], 36 | } 37 | -------------------------------------------------------------------------------- /src/__tests__/component.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import enzyme, { shallow } from 'enzyme' 3 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17' 4 | import withSizeMock from '../with-size' 5 | import SizeMe from '../component' 6 | 7 | enzyme.configure({ adapter: new Adapter() }) 8 | 9 | jest.mock('../with-size.js') 10 | 11 | const noop = () => undefined 12 | 13 | const sizeMeConfig = { 14 | monitorHeight: true, 15 | monitorWidth: true, 16 | refreshRate: 80, 17 | refreshMode: 'debounce', 18 | noPlaceholder: true, 19 | resizeDetectorStrategy: 'foo', 20 | } 21 | 22 | describe('', () => { 23 | beforeEach(() => { 24 | jest.resetAllMocks() 25 | withSizeMock.mockImplementation(() => (Component) => Component) 26 | }) 27 | 28 | it('should pass down props as configuration to withSize', () => { 29 | shallow() 30 | expect(withSizeMock).lastCalledWith(sizeMeConfig) 31 | }) 32 | 33 | it('should monitor and provide the size to the render func', () => { 34 | let actualSize 35 | const wrapper = shallow( 36 | { 38 | actualSize = size 39 | }} 40 | {...sizeMeConfig} 41 | />, 42 | ) 43 | wrapper.prop('onSize')({ width: 100, height: 50 }) 44 | expect(actualSize).toEqual({ width: 100, height: 50 }) 45 | }) 46 | 47 | it('should update the sizeme component when a new configuration is provided', () => { 48 | const wrapper = shallow({noop}) 49 | const newSizeMeConfig = { 50 | ...sizeMeConfig, 51 | monitorHeight: false, 52 | } 53 | wrapper.setProps({ ...sizeMeConfig, ...newSizeMeConfig }) 54 | expect(withSizeMock).toHaveBeenCalledTimes(2) 55 | expect(withSizeMock).lastCalledWith(newSizeMeConfig) 56 | }) 57 | 58 | it('should not update the sizeme component when a new configuration is provided', () => { 59 | const wrapper = shallow() 60 | wrapper.setProps({ render: () => 'NEW!', ...sizeMeConfig }) 61 | expect(withSizeMock).toHaveBeenCalledTimes(1) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/__tests__/typescript.test.js: -------------------------------------------------------------------------------- 1 | import { checkDirectory } from 'typings-tester' 2 | 3 | describe('TypeScript definitions', () => { 4 | it('should compile against easy-peasy.d.ts', () => { 5 | checkDirectory(`${__dirname}/typescript`) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/__tests__/typescript/component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SizeMe } from 'react-sizeme' 3 | 4 | function MyApp() { 5 | return ( 6 | 7 | {({ size }) => { 8 | const { width, height } = size 9 | if (width) { 10 | const foo = width + 1 11 | } 12 | if (height) { 13 | const foo = height + 1 14 | } 15 | // typings:expect-error 16 | const h1 = height + 1 17 | // typings:expect-error 18 | const w1 = width + 1 19 | return

20 | }} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/typescript/hoc.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { withSize, SizeMeProps, WithSizeOnSizeCallback } from 'react-sizeme' 3 | 4 | interface MyComponentProps extends SizeMeProps { 5 | id: number 6 | } 7 | 8 | function MyComponent({ id, size }: MyComponentProps) { 9 | const { width, height } = size 10 | if (width) { 11 | const foo = width + 1 12 | } 13 | if (height) { 14 | const foo = height + 1 15 | } 16 | // typings:expect-error 17 | const h1 = height + 1 18 | // typings:expect-error 19 | const w1 = width + 1 20 | return
My width is {size.width}px
21 | } 22 | 23 | const SizedMyComponent = withSize()(MyComponent) 24 | 25 | const onSize: WithSizeOnSizeCallback = ({ height, width }) => { 26 | if (width) { 27 | const foo = width + 1 28 | } 29 | if (height) { 30 | const foo = height + 1 31 | } 32 | // typings:expect-error 33 | const h1 = height + 1 34 | // typings:expect-error 35 | const w1 = width + 1 36 | } 37 | 38 | const foo = 39 | -------------------------------------------------------------------------------- /src/__tests__/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext", "dom"], 4 | "jsx": "preserve", 5 | "strict": true, 6 | "baseUrl": "../../..", 7 | "paths": { 8 | "react-sizeme": ["react-sizeme.d.ts"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/with-size.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-console */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable global-require */ 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | /* eslint-disable react/jsx-one-expression-per-line */ 7 | 8 | import React from 'react' 9 | import enzyme, { mount } from 'enzyme' 10 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17' 11 | import { renderToStaticMarkup } from 'react-dom/server' 12 | 13 | enzyme.configure({ adapter: new Adapter() }) 14 | 15 | describe('withSize', () => { 16 | let withSize 17 | let resizeDetectorMock 18 | const placeholderHtml = '
' 19 | 20 | const SizeRender = ({ size, debug }) => { 21 | if (size == null) { 22 | return
No given size
23 | } 24 | 25 | const { width, height } = size 26 | const result = ( 27 |
28 | w: {width || 'null'}, h: {height || 'null'} 29 |
30 | ) 31 | if (debug) { 32 | console.log(result) 33 | } 34 | return result 35 | } 36 | 37 | const expected = ({ width, height }) => 38 | `w: ${width || 'null'}, h: ${height || 'null'}` 39 | 40 | const delay = (fn, time) => 41 | new Promise((resolve, reject) => { 42 | setTimeout(() => { 43 | try { 44 | fn() 45 | } catch (err) { 46 | reject(err) 47 | } 48 | resolve() 49 | }, time) 50 | }) 51 | 52 | beforeEach(() => { 53 | resizeDetectorMock = { 54 | // :: domEl => void, 55 | removeAllListeners: jest.fn(), 56 | // :: domEl -> void 57 | uninstall: jest.fn(), 58 | // :: (domeEl, callback) -> void 59 | listenTo: jest.fn(), 60 | } 61 | jest.doMock('../resize-detector.js', () => 62 | jest.fn(() => resizeDetectorMock), 63 | ) 64 | withSize = require('../with-size').default 65 | }) 66 | 67 | describe('When providing a configuration object', () => { 68 | describe('And the refresh rate is below 16', () => { 69 | it('Then an error should be thrown', () => { 70 | const action = () => withSize({ refreshRate: 15 }) 71 | expect(action).toThrow(/don't put your refreshRate lower than 16/) 72 | }) 73 | }) 74 | 75 | describe('And setting an invalid refreshMode to "debounce"', () => { 76 | it('Then an error should be thrown', () => { 77 | const action = () => withSize({ refreshMode: 'foo' }) 78 | expect(action).toThrow(/refreshMode should have a value of/) 79 | }) 80 | }) 81 | 82 | describe('And both monitor values are set to false', () => { 83 | it('Then an error should be thrown', () => { 84 | const action = () => 85 | withSize({ monitorHeight: false, monitorWidth: false }) 86 | expect(action).toThrow( 87 | /You have to monitor at least one of the width or height/, 88 | ) 89 | }) 90 | }) 91 | }) 92 | 93 | describe('When disabling placeholders via the component config', () => { 94 | it('Then the component should render without any size info', () => { 95 | const SizeAwareComponent = withSize({ noPlaceholder: true })(SizeRender) 96 | const mounted = mount() 97 | expect(mounted.text()).toEqual(expected({})) 98 | }) 99 | }) 100 | 101 | describe('When disabling placeholders via the global config', () => { 102 | beforeEach(() => { 103 | withSize.noPlaceholders = true 104 | }) 105 | 106 | afterEach(() => { 107 | withSize.noPlaceholders = false 108 | }) 109 | 110 | it('should not use placeholders when the global config is set', () => { 111 | const SizeAwareComponent = withSize()(SizeRender) 112 | const mounted = mount() 113 | expect(mounted.text()).toEqual(expected({})) 114 | }) 115 | }) 116 | 117 | describe('When using the sizeCallback fn', () => { 118 | it('should pass the size data to the callback and pass down no size prop', () => { 119 | const SizeAwareComponent = withSize({ 120 | monitorHeight: true, 121 | })(SizeRender) 122 | 123 | class SizeCallbackWrapper extends React.Component { 124 | state = { 125 | // eslint-disable-next-line react/no-unused-state 126 | size: null, 127 | } 128 | 129 | onSize = (size) => 130 | this.setState({ 131 | // eslint-disable-next-line react/no-unused-state 132 | size, 133 | }) 134 | 135 | render() { 136 | return 137 | } 138 | } 139 | 140 | const mounted = mount() 141 | 142 | // Get the callback for size changes. 143 | const { listenTo } = resizeDetectorMock 144 | const checkIfSizeChangedCallback = listenTo.mock.calls[0][1] 145 | checkIfSizeChangedCallback({ 146 | getBoundingClientRect: () => ({ 147 | width: 100, 148 | height: 50, 149 | }), 150 | }) 151 | 152 | return delay(() => { 153 | mounted.update() 154 | expect(mounted.state()).toMatchObject({ 155 | size: { width: 100, height: 50 }, 156 | }) 157 | expect(mounted.find(SizeRender).text()).toEqual('No given size') 158 | }, 20) 159 | }) 160 | }) 161 | 162 | describe('When mounting and unmounting the placeholder component', () => { 163 | it('Then the resizeDetector registration and deregistration should be called', () => { 164 | const SizeAwareComponent = withSize()(SizeRender) 165 | 166 | const mounted = mount() 167 | 168 | expect(resizeDetectorMock.listenTo).toHaveBeenCalledTimes(1) 169 | expect(resizeDetectorMock.uninstall).toHaveBeenCalledTimes(0) 170 | 171 | mounted.unmount() 172 | 173 | expect(resizeDetectorMock.listenTo).toHaveBeenCalledTimes(1) 174 | expect(resizeDetectorMock.uninstall).toHaveBeenCalledTimes(1) 175 | }) 176 | }) 177 | 178 | describe('When setting the "debounce" refreshMode', () => { 179 | it('Then the size data should only appear after the refresh rate has expired', () => { 180 | const config = { 181 | refreshMode: 'debounce', 182 | refreshRate: 50, 183 | monitorHeight: true, 184 | } 185 | const SizeAwareComponent = withSize(config)(SizeRender) 186 | 187 | const mounted = mount() 188 | 189 | // Get the callback for size changes. 190 | const { listenTo } = resizeDetectorMock 191 | const checkIfSizeChangedCallback = listenTo.mock.calls[0][1] 192 | checkIfSizeChangedCallback({ 193 | getBoundingClientRect: () => ({ 194 | width: 100, 195 | height: 50, 196 | }), 197 | }) 198 | 199 | return Promise.all([ 200 | delay(() => expect(mounted.text()).toEqual(''), 25), 201 | delay(() => { 202 | mounted.update() 203 | expect(mounted.text()).toEqual(expected({ width: 100, height: 50 })) 204 | }, 60), 205 | ]) 206 | }) 207 | }) 208 | 209 | describe('When the wrapped component gets mounted after the placeholder', () => { 210 | it('Then the resizeDetector registration and deregistration should be called', () => { 211 | const config = { monitorHeight: true } 212 | const SizeAwareComponent = withSize(config)(SizeRender) 213 | 214 | const mounted = mount() 215 | 216 | // An add listener should have been called for the placeholder. 217 | expect(resizeDetectorMock.listenTo).toHaveBeenCalledTimes(1) 218 | expect(resizeDetectorMock.uninstall).toHaveBeenCalledTimes(0) 219 | 220 | // Get the callback for size changes. 221 | const checkIfSizeChangedCallback = 222 | resizeDetectorMock.listenTo.mock.calls[0][1] 223 | checkIfSizeChangedCallback({ 224 | getBoundingClientRect: () => ({ 225 | width: 100, 226 | height: 50, 227 | }), 228 | }) 229 | 230 | // Our actual component should have mounted, therefore a removelistener 231 | // should have been called on the placeholder, and an add listener 232 | // on the newly mounted component. 233 | mounted.update() 234 | expect(mounted.text()).toEqual(expected({ width: 100, height: 50 })) 235 | expect(resizeDetectorMock.listenTo).toHaveBeenCalledTimes(2) 236 | expect(resizeDetectorMock.uninstall).toHaveBeenCalledTimes(1) 237 | 238 | // umount 239 | mounted.unmount() 240 | 241 | // The remove listener should have been called! 242 | expect(resizeDetectorMock.listenTo).toHaveBeenCalledTimes(2) 243 | expect(resizeDetectorMock.uninstall).toHaveBeenCalledTimes(2) 244 | }) 245 | }) 246 | 247 | describe('When no className or style has been provided', () => { 248 | it('Then it should render the default placeholder', () => { 249 | const SizeAwareComponent = withSize()(SizeRender) 250 | const mounted = mount() 251 | expect(mounted.html()).toEqual(placeholderHtml) 252 | }) 253 | }) 254 | 255 | describe('When only a className has been provided', () => { 256 | it('Then it should render a placeholder with the className', () => { 257 | const SizeAwareComponent = withSize()(SizeRender) 258 | const mounted = mount() 259 | expect(mounted.html()).toEqual('
') 260 | }) 261 | }) 262 | 263 | describe('When only a style has been provided', () => { 264 | it('Then it should render a placeholder with the style', () => { 265 | const SizeAwareComponent = withSize()(SizeRender) 266 | const mounted = mount() 267 | expect(mounted.html()).toEqual('
') 268 | }) 269 | }) 270 | 271 | describe('When a className and style have been provided', () => { 272 | it('Then it should render a placeholder with both', () => { 273 | const SizeAwareComponent = withSize()(SizeRender) 274 | const mounted = mount( 275 | , 276 | ) 277 | expect(mounted.html()).toEqual( 278 | '
', 279 | ) 280 | }) 281 | }) 282 | 283 | describe('When the size event has occurred when only width is being monitored', () => { 284 | it('Then expected sizes should be provided to the rendered component', () => { 285 | const SizeAwareComponent = withSize({ 286 | monitorWidth: true, 287 | monitorHeight: false, 288 | })(SizeRender) 289 | 290 | const mounted = mount() 291 | 292 | // Initial render should be as expected. 293 | expect(mounted.html()).toEqual(placeholderHtml) 294 | 295 | // Get the callback for size changes. 296 | const checkIfSizeChangedCallback = 297 | resizeDetectorMock.listenTo.mock.calls[0][1] 298 | checkIfSizeChangedCallback({ 299 | getBoundingClientRect: () => ({ width: 100, height: 150 }), 300 | }) 301 | 302 | // Update should have occurred immediately. 303 | mounted.update() 304 | expect(mounted.text()).toEqual(expected({ width: 100 })) 305 | }) 306 | }) 307 | 308 | describe('When the size event has occurred when only height is being monitored', () => { 309 | it('Then expected sizes should be provided to the rendered component', () => { 310 | const SizeAwareComponent = withSize({ 311 | monitorWidth: false, 312 | monitorHeight: true, 313 | })(SizeRender) 314 | 315 | const mounted = mount() 316 | 317 | // Initial render should be as expected. 318 | expect(mounted.html()).toEqual(placeholderHtml) 319 | 320 | // Get the callback for size changes. 321 | const checkIfSizeChangedCallback = 322 | resizeDetectorMock.listenTo.mock.calls[0][1] 323 | checkIfSizeChangedCallback({ 324 | getBoundingClientRect: () => ({ width: 100, height: 150 }), 325 | }) 326 | 327 | // Update should have occurred immediately. 328 | mounted.update() 329 | expect(mounted.text()).toEqual(expected({ height: 150 })) 330 | }) 331 | }) 332 | 333 | describe('When the size event has occurred when width and height are being monitored', () => { 334 | it('Then expected sizes should be provided to the rendered component', () => { 335 | const SizeAwareComponent = withSize({ 336 | monitorWidth: true, 337 | monitorHeight: true, 338 | })(SizeRender) 339 | 340 | const mounted = mount() 341 | 342 | // Initial render should be as expected. 343 | expect(mounted.html()).toEqual(placeholderHtml) 344 | 345 | // Get the callback for size changes. 346 | const checkIfSizeChangedCallback = 347 | resizeDetectorMock.listenTo.mock.calls[0][1] 348 | checkIfSizeChangedCallback({ 349 | getBoundingClientRect: () => ({ width: 100, height: 150 }), 350 | }) 351 | 352 | // Update should have occurred immediately. 353 | mounted.update() 354 | expect(mounted.text()).toEqual(expected({ height: 150, width: 100 })) 355 | }) 356 | }) 357 | 358 | describe('When it receives new non-size props', () => { 359 | it('Then the new props should be passed into the component', () => { 360 | const SizeAwareComponent = withSize({ 361 | monitorHeight: true, 362 | monitorWidth: true, 363 | })(({ otherProp }) =>
{otherProp}
) 364 | 365 | const mounted = mount() 366 | 367 | // Get the callback for size changes. 368 | const checkIfSizeChangedCallback = 369 | resizeDetectorMock.listenTo.mock.calls[0][1] 370 | checkIfSizeChangedCallback({ 371 | getBoundingClientRect: () => ({ 372 | width: 100, 373 | height: 100, 374 | }), 375 | }) 376 | 377 | mounted.update() 378 | expect(mounted.text()).toEqual('foo') 379 | mounted.setProps({ otherProp: 'bar' }) 380 | expect(mounted.text()).toEqual('bar') 381 | }) 382 | }) 383 | 384 | describe('When running is SSR mode', () => { 385 | beforeEach(() => { 386 | withSize.enableSSRBehaviour = true 387 | }) 388 | 389 | it('Then it should render the wrapped rather than the placeholder', () => { 390 | const SizeAwareComponent = withSize({ 391 | monitorHeight: true, 392 | monitorWidth: true, 393 | })(SizeRender) 394 | 395 | const actual = renderToStaticMarkup( 396 | , 397 | ) 398 | 399 | expect(actual).toContain(expected({})) 400 | }) 401 | }) 402 | }) 403 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | 3 | import React, { Component } from 'react' 4 | import isShallowEqual from 'shallowequal' 5 | import withSize from './with-size' 6 | 7 | export default class SizeMe extends Component { 8 | static defaultProps = { 9 | children: undefined, 10 | render: undefined, 11 | } 12 | 13 | constructor(props) { 14 | super(props) 15 | const { children, render, ...sizeMeConfig } = props 16 | this.createComponent(sizeMeConfig) 17 | this.state = { 18 | size: { 19 | width: undefined, 20 | height: undefined, 21 | }, 22 | } 23 | } 24 | 25 | componentDidUpdate(prevProps) { 26 | const { 27 | children: prevChildren, 28 | render: prevRender, 29 | ...currentSizeMeConfig 30 | } = this.props 31 | const { 32 | children: nextChildren, 33 | render: nextRender, 34 | ...prevSizeMeConfig 35 | } = prevProps 36 | if (!isShallowEqual(currentSizeMeConfig, prevSizeMeConfig)) { 37 | this.createComponent(currentSizeMeConfig) 38 | } 39 | } 40 | 41 | createComponent = (config) => { 42 | this.SizeAware = withSize(config)(({ children }) => children) 43 | } 44 | 45 | onSize = (size) => this.setState({ size }) 46 | 47 | render() { 48 | const { SizeAware } = this 49 | const render = this.props.children || this.props.render 50 | return ( 51 | 52 | {render({ size: this.state.size })} 53 | 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import withSize from './with-size' 2 | import SizeMe from './component' 3 | 4 | withSize.SizeMe = SizeMe 5 | withSize.withSize = withSize 6 | 7 | export default withSize 8 | -------------------------------------------------------------------------------- /src/resize-detector.js: -------------------------------------------------------------------------------- 1 | import createResizeDetector from 'element-resize-detector' 2 | 3 | const instances = {} 4 | 5 | // Lazily require to not cause bug 6 | // https://github.com/ctrlplusb/react-sizeme/issues/6 7 | function resizeDetector(strategy = 'scroll') { 8 | if (!instances[strategy]) { 9 | instances[strategy] = createResizeDetector({ 10 | strategy, 11 | }) 12 | } 13 | 14 | return instances[strategy] 15 | } 16 | 17 | export default resizeDetector 18 | -------------------------------------------------------------------------------- /src/with-size.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable react/require-default-props */ 4 | /* eslint-disable react/no-find-dom-node */ 5 | 6 | import React, { Children, Component } from 'react' 7 | import ReactDOM from 'react-dom' 8 | import invariant from 'invariant' 9 | import { debounce, throttle } from 'throttle-debounce' 10 | import resizeDetector from './resize-detector' 11 | 12 | const errMsg = 13 | 'react-sizeme: an error occurred whilst stopping to listen to node size changes' 14 | 15 | const defaultConfig = { 16 | monitorWidth: true, 17 | monitorHeight: false, 18 | refreshRate: 16, 19 | refreshMode: 'throttle', 20 | noPlaceholder: false, 21 | resizeDetectorStrategy: 'scroll', 22 | } 23 | 24 | function getDisplayName(WrappedComponent) { 25 | return WrappedComponent.displayName || WrappedComponent.name || 'Component' 26 | } 27 | 28 | /** 29 | * This is a utility wrapper component that will allow our higher order 30 | * component to get a ref handle on our wrapped components html. 31 | * @see https://gist.github.com/jimfb/32b587ee6177665fb4cf 32 | */ 33 | class ReferenceWrapper extends Component { 34 | static displayName = 'SizeMeReferenceWrapper' 35 | 36 | render() { 37 | return Children.only(this.props.children) 38 | } 39 | } 40 | 41 | function Placeholder({ className, style }) { 42 | // Lets create the props for the temp element. 43 | const phProps = {} 44 | 45 | // We will use any provided className/style or else make the temp 46 | // container take the full available space. 47 | if (!className && !style) { 48 | phProps.style = { width: '100%', height: '100%' } 49 | } else { 50 | if (className) { 51 | phProps.className = className 52 | } 53 | if (style) { 54 | phProps.style = style 55 | } 56 | } 57 | 58 | return
59 | } 60 | Placeholder.displayName = 'SizeMePlaceholder' 61 | 62 | /** 63 | * As we need to maintain a ref on the root node that is rendered within our 64 | * SizeMe component we need to wrap our entire render in a sub component. 65 | * Without this, we lose the DOM ref after the placeholder is removed from 66 | * the render and the actual component is rendered. 67 | * It took me forever to figure this out, so tread extra careful on this one! 68 | */ 69 | const renderWrapper = (WrappedComponent) => { 70 | function SizeMeRenderer(props) { 71 | const { 72 | explicitRef, 73 | className, 74 | style, 75 | size, 76 | disablePlaceholder, 77 | onSize, 78 | ...restProps 79 | } = props 80 | 81 | const noSizeData = 82 | size == null || (size.width == null && size.height == null) 83 | 84 | const renderPlaceholder = noSizeData && !disablePlaceholder 85 | 86 | const renderProps = { 87 | className, 88 | style, 89 | } 90 | 91 | if (size != null) { 92 | renderProps.size = size 93 | } 94 | 95 | const toRender = renderPlaceholder ? ( 96 | 97 | ) : ( 98 | 99 | ) 100 | 101 | return {toRender} 102 | } 103 | 104 | SizeMeRenderer.displayName = `SizeMeRenderer(${getDisplayName( 105 | WrappedComponent, 106 | )})` 107 | 108 | return SizeMeRenderer 109 | } 110 | 111 | /** 112 | * :: config -> Component -> WrappedComponent 113 | * 114 | * Higher order component that allows the wrapped component to become aware 115 | * of it's size, by receiving it as an object within it's props. 116 | * 117 | * @param monitorWidth 118 | * Default true, whether changes in the element's width should be monitored, 119 | * causing a size property to be broadcast. 120 | * @param monitorHeight 121 | * Default false, whether changes in the element's height should be monitored, 122 | * causing a size property to be broadcast. 123 | * 124 | * @return The wrapped component. 125 | */ 126 | function withSize(config = defaultConfig) { 127 | const { 128 | monitorWidth = defaultConfig.monitorWidth, 129 | monitorHeight = defaultConfig.monitorHeight, 130 | refreshRate = defaultConfig.refreshRate, 131 | refreshMode = defaultConfig.refreshMode, 132 | noPlaceholder = defaultConfig.noPlaceholder, 133 | resizeDetectorStrategy = defaultConfig.resizeDetectorStrategy, 134 | } = config 135 | 136 | invariant( 137 | monitorWidth || monitorHeight, 138 | 'You have to monitor at least one of the width or height when using "sizeMe"', 139 | ) 140 | 141 | invariant( 142 | refreshRate >= 16, 143 | "It is highly recommended that you don't put your refreshRate lower than " + 144 | '16 as this may cause layout thrashing.', 145 | ) 146 | 147 | invariant( 148 | refreshMode === 'throttle' || refreshMode === 'debounce', 149 | 'The refreshMode should have a value of "throttle" or "debounce"', 150 | ) 151 | 152 | const refreshDelayStrategy = refreshMode === 'throttle' ? throttle : debounce 153 | 154 | return function WrapComponent(WrappedComponent) { 155 | const SizeMeRenderWrapper = renderWrapper(WrappedComponent) 156 | 157 | class SizeAwareComponent extends React.Component { 158 | static displayName = `SizeMe(${getDisplayName(WrappedComponent)})` 159 | 160 | domEl = null 161 | 162 | state = { 163 | width: undefined, 164 | height: undefined, 165 | } 166 | 167 | componentDidMount() { 168 | this.detector = resizeDetector(resizeDetectorStrategy) 169 | this.determineStrategy(this.props) 170 | this.handleDOMNode() 171 | } 172 | 173 | componentDidUpdate() { 174 | this.determineStrategy(this.props) 175 | this.handleDOMNode() 176 | } 177 | 178 | componentWillUnmount() { 179 | // Change our size checker to a noop just in case we have some 180 | // late running events. 181 | this.hasSizeChanged = () => undefined 182 | this.checkIfSizeChanged = () => undefined 183 | this.uninstall() 184 | } 185 | 186 | uninstall = () => { 187 | if (this.domEl) { 188 | try { 189 | this.detector.uninstall(this.domEl) 190 | } catch (err) { 191 | // eslint-disable-next-line no-console 192 | console.warn(errMsg) 193 | } 194 | this.domEl = null 195 | } 196 | } 197 | 198 | determineStrategy = (props) => { 199 | if (props.onSize) { 200 | if (!this.callbackState) { 201 | this.callbackState = { 202 | ...this.state, 203 | } 204 | } 205 | this.strategy = 'callback' 206 | } else { 207 | this.strategy = 'render' 208 | } 209 | } 210 | 211 | strategisedSetState = (state) => { 212 | if (this.strategy === 'callback') { 213 | this.callbackState = state 214 | this.props.onSize(state) 215 | } 216 | this.setState(state) 217 | } 218 | 219 | strategisedGetState = () => 220 | this.strategy === 'callback' ? this.callbackState : this.state 221 | 222 | handleDOMNode() { 223 | const found = this.element && ReactDOM.findDOMNode(this.element) 224 | 225 | if (!found) { 226 | // If we previously had a dom node then we need to ensure that 227 | // we remove any existing listeners to avoid memory leaks. 228 | this.uninstall() 229 | return 230 | } 231 | 232 | if (!this.domEl) { 233 | this.domEl = found 234 | this.detector.listenTo(this.domEl, this.checkIfSizeChanged) 235 | } else if ( 236 | (this.domEl.isSameNode && !this.domEl.isSameNode(found)) || 237 | this.domEl !== found 238 | ) { 239 | this.uninstall() 240 | this.domEl = found 241 | this.detector.listenTo(this.domEl, this.checkIfSizeChanged) 242 | } else { 243 | // Do nothing 👍 244 | } 245 | } 246 | 247 | refCallback = (element) => { 248 | this.element = element 249 | } 250 | 251 | hasSizeChanged = (current, next) => { 252 | const c = current 253 | const n = next 254 | 255 | return ( 256 | (monitorWidth && c.width !== n.width) || 257 | (monitorHeight && c.height !== n.height) 258 | ) 259 | } 260 | 261 | checkIfSizeChanged = refreshDelayStrategy(refreshRate, (el) => { 262 | const { width, height } = el.getBoundingClientRect() 263 | 264 | const next = { 265 | width: monitorWidth ? width : null, 266 | height: monitorHeight ? height : null, 267 | } 268 | 269 | if (this.hasSizeChanged(this.strategisedGetState(), next)) { 270 | this.strategisedSetState(next) 271 | } 272 | }) 273 | 274 | render() { 275 | const disablePlaceholder = 276 | withSize.enableSSRBehaviour || 277 | withSize.noPlaceholders || 278 | noPlaceholder || 279 | this.strategy === 'callback' 280 | 281 | const size = { ...this.state } 282 | 283 | return ( 284 | 290 | ) 291 | } 292 | } 293 | 294 | SizeAwareComponent.WrappedComponent = WrappedComponent 295 | 296 | return SizeAwareComponent 297 | } 298 | } 299 | 300 | /** 301 | * Allow SizeMe to run within SSR environments. This is a "global" behaviour 302 | * flag that should be set within the initialisation phase of your application. 303 | * 304 | * Warning: don't set this flag unless you need to as using it may cause 305 | * extra render cycles to happen within your components depending on the logic 306 | * contained within them around the usage of the `size` data. 307 | * 308 | * DEPRECATED: Please use the global noPlaceholders 309 | */ 310 | withSize.enableSSRBehaviour = false 311 | 312 | /** 313 | * Global configuration allowing to disable placeholder rendering for all 314 | * sizeMe components. 315 | */ 316 | withSize.noPlaceholders = false 317 | 318 | export default withSize 319 | -------------------------------------------------------------------------------- /tools/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "import/no-extraneous-dependencies": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/scripts/build.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs') 2 | const { inInstall } = require('in-publish') 3 | const prettyBytes = require('pretty-bytes') 4 | const gzipSize = require('gzip-size') 5 | const { pipe } = require('ramda') 6 | const { exec } = require('../utils') 7 | const packageJson = require('../../package.json') 8 | 9 | if (inInstall()) { 10 | process.exit(0) 11 | } 12 | 13 | const nodeEnv = Object.assign({}, process.env, { 14 | NODE_ENV: 'production', 15 | }) 16 | 17 | exec('npx rollup -c rollup-min.config.js', nodeEnv) 18 | exec('npx rollup -c rollup.config.js', nodeEnv) 19 | 20 | function fileGZipSize(path) { 21 | return pipe(readFileSync, gzipSize.sync, prettyBytes)(path) 22 | } 23 | 24 | console.log( 25 | `\ngzipped, the build is ${fileGZipSize(`dist/${packageJson.name}.min.js`)}`, 26 | ) 27 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const appRootDir = require('app-root-dir') 3 | 4 | function exec(command) { 5 | execSync(command, { stdio: 'inherit', cwd: appRootDir.get() }) 6 | } 7 | 8 | module.exports = { 9 | exec, 10 | } 11 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | process.env.NODE_ENV = 'test' 5 | 6 | const babelConfigContents = fs.readFileSync(path.join(__dirname, '.babelrc')) 7 | const babelConfig = JSON.parse(babelConfigContents) 8 | 9 | module.exports = wallaby => ({ 10 | files: ['src/**/*.js', { pattern: 'src/**/*.test.js', ignore: true }], 11 | tests: ['src/**/*.test.js'], 12 | testFramework: 'jest', 13 | env: { 14 | type: 'node', 15 | runner: 'node', 16 | }, 17 | compilers: { 18 | 'src/**/*.js': wallaby.compilers.babel(babelConfig), 19 | }, 20 | }) 21 | --------------------------------------------------------------------------------