├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── src └── index.js ├── testSetup.js ├── tests └── index.js └── wallaby.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-1" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | continuation_indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | quote_type = single 14 | spaces_around_operators = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-okonet", 3 | "env": { 4 | "browser": true, 5 | "mocha": true 6 | }, 7 | "plugins": [ 8 | "import" 9 | ], 10 | "settings": { 11 | "import/resolve": { 12 | "moduleDirectory": ["node_modules"] 13 | } 14 | }, 15 | "rules": { 16 | "react/no-find-dom-node": 0, 17 | "import/no-extraneous-dependencies": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | .idea 36 | 37 | lib 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '6' 9 | before_script: 10 | - npm prune 11 | after_success: 12 | - npm run semantic-release 13 | branches: 14 | except: 15 | - /^v\d+\.\d+\.\d+$/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrey Okonetchnikov 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 | # react-container-dimensions [![Build Status](https://travis-ci.org/okonet/react-container-dimensions.svg?branch=master)](https://travis-ci.org/okonet/react-container-dimensions) [![npm version](https://badge.fury.io/js/react-container-dimensions.svg)](https://badge.fury.io/js/react-container-dimensions) 2 | Wrapper component that detects parent (container) element resize and passes new dimensions down the 3 | tree. Based on [element-resize-detector](https://github.com/wnr/element-resize-detector). 4 | 5 | `npm install --save react-container-dimensions` 6 | 7 | It is especially useful when you create components with dimensions that change over 8 | time and you want to explicitely pass the container dimensions to the children. For example, SVG 9 | visualization needs to be updated in order to fit into container. 10 | 11 | It uses [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) and passes values for all `top`, `right`, `bottom`, `left`, `width`, `height` CSs attributes down the tree. 12 | 13 | ## Usage 14 | 15 | * Wrap your existing components. Children component will recieve `top`, `right`, `bottom`, `left`, `width`, `height` as props. 16 | 17 | ```jsx 18 | 19 | 20 | 21 | ``` 22 | 23 | * Use a function to pass width or height explicitely or do some calculation. Function callback will be called with an object `{ width: number, height: number }` as an argument and it expects the output to be a React Component or an element. 24 | 25 | ```jsx 26 | 27 | { ({ height }) => } 28 | 29 | ``` 30 | 31 | ## How is it different from [similar_component_here] 32 | 33 | *It does not create a new element in the DOM but relies on the `parentNode` which must be present.* So, basically, it acts as a middleware to pass the dimensions of _your_ styled component to your children components. This makes it _very easy_ to integrate with your existing code base. 34 | 35 | For example, if your parent container has `display: flex`, only adjacent children will be affected by this rule. This means if your children rely on `flex` CSS property, you can't wrap it in a div anymore since _this will break the flexbox flow_. 36 | 37 | So this won't work anymore: 38 | 39 | ```html 40 |
41 |
42 |
...
43 |
44 |
45 | ``` 46 | 47 | `react-container-dimensions` doesn't change the resulting HTML markup, so it remains: 48 | 49 | ```html 50 |
51 |
...
52 |
53 | ``` 54 | 55 | ## Example 56 | 57 | Let's say you want your SVG visualization to always fit into the container. In order for SVG to scale elements properly it is required that `width` and `height` attributes are properly set on the `svg` element. Imagine the following example 58 | 59 | ### Before (static) 60 | 61 | It's hard to keep dimensions of the container and the SVG in sync. Especially, when you want your content to be resplonsive (or dynamic). 62 | 63 | ```jsx 64 | export const myVis = () => ( 65 |
66 | 67 | {/* SVG contents */} 68 | 69 |
70 | ) 71 | ``` 72 | 73 | ### After (dynamic) 74 | 75 | This will resize and re-render the SVG each time the `div` dimensions are changed. For instance, when you change CSS for `.myStyles`. 76 | 77 | ```jsx 78 | import ContainerDimensions from 'react-container-dimensions' 79 | 80 | export const myVis = () => ( 81 |
82 | 83 | { ({ width, height }) => 84 | 85 | {/* SVG contents */} 86 | 87 | } 88 | 89 |
90 | ) 91 | ``` 92 | 93 | ## Other similar projects: 94 | 95 | * https://github.com/digidem/react-dimensions 96 | * https://github.com/maslianok/react-resize-detector 97 | * https://github.com/Xananax/react-size 98 | * https://github.com/joeybaker/react-element-query 99 | 100 | and a few others... 101 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface Dimensions { 4 | top: number; 5 | right: number; 6 | bottom: number; 7 | left: number; 8 | width: number; 9 | height: number; 10 | } 11 | 12 | export interface ContainerDimensionsProps { 13 | /** 14 | * Can either be a function that's responsible for rendering children. 15 | * This function should implement the following signature: 16 | * ({ height, width }) => PropTypes.element 17 | * Or a React element, with width and height injected into its props. 18 | */ 19 | children: ((props: Dimensions) => React.ReactNode) | React.ReactNode; 20 | } 21 | 22 | /** 23 | * Component that automatically adjusts the width and height of a single child. 24 | * Child component should not be declared as a child but should rather be specified by a `ChildComponent` property. 25 | * All other properties will be passed through to the child component. 26 | */ 27 | declare const ContainerDimensions: React.ComponentType; 28 | export default ContainerDimensions; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-container-dimensions", 3 | "version": "0.0.0-development", 4 | "description": "Wrapper component that detects element resize and passes new dimensions down the tree. Based on [element-resize-detector](https://github.com/wnr/element-resize-detector)", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel --presets=react,es2015,stage-1 src --out-dir lib", 8 | "clean": "rimraf lib", 9 | "lint": "eslint ./src", 10 | "prepublish": "npm run lint && npm run clean && npm run build", 11 | "test": "mocha --compilers js:babel-core/register --require testSetup.js --recursive ./tests/*.js", 12 | "test:watch": "npm test -- --watch", 13 | "lint-staged": "lint-staged", 14 | "deps": "npm-check -s", 15 | "deps:update": "npm-check -u", 16 | "release": "npmpub", 17 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 18 | }, 19 | "lint-staged": { 20 | "*.js": [ 21 | "eslint --fix", 22 | "git add" 23 | ] 24 | }, 25 | "pre-commit": "lint-staged", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/okonet/react-container-dimensions.git" 29 | }, 30 | "files": [ 31 | "lib", 32 | "index.d.ts" 33 | ], 34 | "directories": { 35 | "lib": "lib" 36 | }, 37 | "keywords": [ 38 | "resize", 39 | "parent", 40 | "container", 41 | "element", 42 | "react", 43 | "detector", 44 | "detect", 45 | "size", 46 | "dimensions" 47 | ], 48 | "author": "Andrey Okonetchnikov ", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/okonet/react-container-dimensions/issues" 52 | }, 53 | "homepage": "https://github.com/okonet/react-container-dimensions#readme", 54 | "dependencies": { 55 | "element-resize-detector": "^1.1.10", 56 | "invariant": "^2.2.2", 57 | "prop-types": "^15.5.8" 58 | }, 59 | "peerDependencies": { 60 | "react": "^0.14.0 || ^15.0.0 || 16.x", 61 | "react-dom": "^0.14.0 || ^15.0.0 || 16.x" 62 | }, 63 | "devDependencies": { 64 | "babel": "^6.23.0", 65 | "babel-cli": "^6.23.0", 66 | "babel-preset-es2015": "^6.22.0", 67 | "babel-preset-react": "^6.23.0", 68 | "babel-preset-stage-1": "^6.22.0", 69 | "chai": "^3.5.0", 70 | "chai-enzyme": "^0.6.1", 71 | "enzyme": "^2.7.1", 72 | "eslint": "^3.11.0", 73 | "eslint-config-okonet": "^1.2.3", 74 | "jsdom": "^9.11.0", 75 | "lint-staged": "^4.0.0", 76 | "mocha": "^3.2.0", 77 | "npm-check": "^5.2.2", 78 | "npmpub": "^3.1.0", 79 | "pre-commit": "^1.1.2", 80 | "prettier": "^1.5.3", 81 | "react": "^15.4.1", 82 | "react-addons-test-utils": "^15.4.1", 83 | "react-dom": "^15.4.1", 84 | "rimraf": "^2.5.3", 85 | "semantic-release": "^6.3.2", 86 | "sinon": "^2.2.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Children, Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ReactDOM from 'react-dom' 4 | import elementResizeDetectorMaker from 'element-resize-detector' 5 | import invariant from 'invariant' 6 | 7 | export default class ContainerDimensions extends Component { 8 | 9 | static propTypes = { 10 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired 11 | } 12 | 13 | static getDomNodeDimensions(node) { 14 | const { top, right, bottom, left, width, height } = node.getBoundingClientRect() 15 | return { top, right, bottom, left, width, height } 16 | } 17 | 18 | constructor() { 19 | super() 20 | this.state = { 21 | initiated: false 22 | } 23 | this.onResize = this.onResize.bind(this) 24 | } 25 | 26 | componentDidMount() { 27 | this.parentNode = ReactDOM.findDOMNode(this).parentNode 28 | this.elementResizeDetector = elementResizeDetectorMaker({ 29 | strategy: 'scroll', 30 | callOnAdd: false 31 | }) 32 | this.elementResizeDetector.listenTo(this.parentNode, this.onResize) 33 | this.componentIsMounted = true 34 | this.onResize() 35 | } 36 | 37 | componentWillUnmount() { 38 | this.componentIsMounted = false 39 | this.elementResizeDetector.uninstall(this.parentNode) 40 | } 41 | 42 | onResize() { 43 | const clientRect = ContainerDimensions.getDomNodeDimensions(this.parentNode) 44 | if (this.componentIsMounted) { 45 | this.setState({ 46 | initiated: true, 47 | ...clientRect 48 | }) 49 | } 50 | } 51 | 52 | render() { 53 | invariant(this.props.children, 'Expected children to be one of function or React.Element') 54 | 55 | if (!this.state.initiated) { 56 | return
57 | } 58 | if (typeof this.props.children === 'function') { 59 | const renderedChildren = this.props.children(this.state) 60 | return renderedChildren && Children.only(renderedChildren) 61 | } 62 | return Children.only(React.cloneElement(this.props.children, this.state)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /testSetup.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom').jsdom 2 | 3 | global.document = jsdom('
') 4 | global.window = document.defaultView 5 | 6 | Object.keys(document.defaultView).forEach((property) => { 7 | if (typeof global[property] === 'undefined') { 8 | global[property] = document.defaultView[property] 9 | } 10 | }) 11 | 12 | global.navigator = { userAgent: 'node.js' } 13 | global.documentRef = document 14 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import React from 'react' 3 | import { mount } from 'enzyme' 4 | import { spy, stub } from 'sinon' 5 | import chai, { expect } from 'chai' 6 | import ContainerDimensions from '../src/index' 7 | 8 | chai.use(require('chai-enzyme')()) 9 | const MyComponent = ({ width, height }) => {width}, {height} // eslint-disable-line 10 | 11 | describe('react-container-dimensions', () => { 12 | 13 | it('should throw without children', () => { 14 | expect(() => mount(
)) 15 | .to.throw('Expected children to be one of function or React.Element') 16 | }) 17 | 18 | it('calls componentDidMount', (done) => { 19 | spy(ContainerDimensions.prototype, 'componentDidMount') 20 | spy(ContainerDimensions.prototype, 'componentWillUnmount') 21 | const wrapper = mount( 22 |
23 | 24 | 25 | 26 |
27 | , { attachTo: document.getElementById('root') }) 28 | expect(wrapper.find('div').length).to.eq(1) 29 | expect(ContainerDimensions.prototype.componentDidMount.calledOnce).to.be.true 30 | ContainerDimensions.prototype.componentDidMount.restore() 31 | setTimeout(() => { 32 | wrapper.unmount() 33 | expect(ContainerDimensions.prototype.componentWillUnmount.calledOnce).to.be.true 34 | ContainerDimensions.prototype.componentWillUnmount.restore() 35 | done() 36 | }, 0) 37 | }) 38 | 39 | it('calls onResize on mount', () => { 40 | spy(ContainerDimensions.prototype, 'onResize') 41 | mount( 42 |
43 | 44 | 45 | 46 |
47 | ) 48 | expect(ContainerDimensions.prototype.onResize.calledOnce).to.be.true 49 | ContainerDimensions.prototype.onResize.restore() 50 | }) 51 | 52 | xit('calls onResize when parent has been resized', (done) => { 53 | spy(ContainerDimensions.prototype, 'onResize') 54 | const wrapper = mount( 55 |
56 | 57 | 58 | 59 |
60 | , { attachTo: document.getElementById('root') }) 61 | const el = wrapper.render() 62 | el.css('width', 10) 63 | setTimeout(() => { 64 | el.css('width', 100) // Triggering onResize event 65 | expect(ContainerDimensions.prototype.onResize.calledTwice).to.be.true 66 | ContainerDimensions.prototype.onResize.restore() 67 | done() 68 | }, 10) 69 | }) 70 | 71 | it('onResize sets state with all keys and values from getBoundingClientRect', () => { 72 | const styles = { top: 100, width: 200 } 73 | stub(ContainerDimensions, 'getDomNodeDimensions', () => ({ 74 | top: 0, 75 | right: 0, 76 | bottom: 0, 77 | left: 0, 78 | width: 0, 79 | height: 0, 80 | ...styles 81 | })) 82 | const wrapper = mount( 83 |
84 | 85 | 86 | 87 |
88 | , { attachTo: document.getElementById('root') }) 89 | 90 | wrapper.render() 91 | expect(wrapper.find(MyComponent).props()).to.have.keys([ 92 | 'initiated', 93 | 'top', 94 | 'right', 95 | 'bottom', 96 | 'left', 97 | 'width', 98 | 'height']) 99 | expect(wrapper.find(MyComponent)).to.have.prop('top', 100) 100 | expect(wrapper.find(MyComponent)).to.have.prop('width', 200) 101 | ContainerDimensions.getDomNodeDimensions.restore() 102 | }) 103 | 104 | it('should initially render an empty placeholder', () => { 105 | const wrapper = mount( 106 | 107 | 108 | 109 | ) 110 | wrapper.setState({ 111 | initiated: false 112 | }) 113 | 114 | expect(wrapper.find(ContainerDimensions)).to.have.exactly(1).descendants('div') 115 | expect(wrapper).to.have.html('
') 116 | expect(wrapper.find(MyComponent).length).to.eq(0) 117 | 118 | wrapper.setState({ 119 | initiated: true 120 | }) 121 | 122 | expect(wrapper.find(MyComponent).length).to.eq(1) 123 | }) 124 | 125 | it('should pass dimensions as props to its children', () => { 126 | const wrapper = mount( 127 | 128 | 129 | 130 | ) 131 | wrapper.setState({ 132 | top: 0, 133 | right: 100, 134 | bottom: 300, 135 | left: 200, 136 | width: 100, 137 | height: 200 138 | }) 139 | expect(wrapper.find(MyComponent)).to.have.length(1) 140 | expect(wrapper.find(MyComponent)).to.have.prop('top', 0) 141 | expect(wrapper.find(MyComponent)).to.have.prop('right', 100) 142 | expect(wrapper.find(MyComponent)).to.have.prop('bottom', 300) 143 | expect(wrapper.find(MyComponent)).to.have.prop('left', 200) 144 | expect(wrapper.find(MyComponent)).to.have.prop('width', 100) 145 | expect(wrapper.find(MyComponent)).to.have.prop('height', 200) 146 | }) 147 | 148 | it('should pass dimensions as function arguments', () => { 149 | const wrapper = mount( 150 | 151 | { 152 | ({ left, width, height }) => // eslint-disable-line 153 | 158 | } 159 | 160 | ) 161 | wrapper.setState({ 162 | left: 20, 163 | width: 100, 164 | height: 200 165 | }) 166 | expect(wrapper.find(MyComponent)).to.have.length(1) 167 | expect(wrapper.find(MyComponent)).to.have.prop('left', 30) 168 | expect(wrapper.find(MyComponent)).to.have.prop('width', 110) 169 | expect(wrapper.find(MyComponent)).to.have.prop('height', 210) 170 | }) 171 | 172 | it('should work with SVG elements', () => { 173 | const wrapper = mount( 174 | 175 | 176 | 177 | ) 178 | wrapper.setState({ 179 | width: 100, 180 | height: 200 181 | }) 182 | expect(wrapper.find('rect')).to.have.length(1) 183 | expect(wrapper.find('rect')).to.have.prop('width', 100) 184 | expect(wrapper.find('rect')).to.have.prop('height', 200) 185 | }) 186 | 187 | it('should not create a DOM element for itself', () => { 188 | const wrapper = mount( 189 |

190 | 191 | Test 192 | 193 |

194 | ) 195 | expect(wrapper.html()).to.contain('

Test') 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /wallaby.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | files: [ 4 | 'testSetup.js', 5 | 'src/*.js' 6 | ], 7 | 8 | tests: ['tests/*.js'], 9 | 10 | env: { 11 | type: 'node', 12 | runner: 'node' 13 | }, 14 | 15 | compilers: { 16 | '**/*.js': wallaby.compilers.babel() 17 | }, 18 | 19 | testFramework: 'mocha', 20 | 21 | bootstrap: function bootstrap() { 22 | require('./testSetup') 23 | } 24 | 25 | } 26 | } 27 | --------------------------------------------------------------------------------