diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index ecfbd6697..e8e7944e3 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -9,6 +9,7 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import debounce from 'debounce'; +import findNodeHandle from '../../exports/findNodeHandle'; const emptyObject = {}; const registry = {}; @@ -16,22 +17,66 @@ const registry = {}; let id = 1; const guid = () => `r-${id++}`; +let resizeObserver; if (canUseDOM) { - const triggerAll = () => { - Object.keys(registry).forEach(key => { - const instance = registry[key]; - instance._handleLayout(); + if (typeof window.ResizeObserver !== 'undefined') { + resizeObserver = new window.ResizeObserver(entries => { + entries.forEach(({ target }) => { + const instance = registry[target._onLayoutId]; + instance && instance._handleLayout(); + }); }); - }; + } else { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'onLayout relies on ResizeObserver which is not supported by your browser. ' + + 'Please include a polyfill. https://github.com/WICG/ResizeObserver/issues/3. ' + + 'Falling back to window.onresize.' + ); + } - window.addEventListener('resize', debounce(triggerAll, 16), false); + const triggerAll = () => { + Object.keys(registry).forEach(key => { + const instance = registry[key]; + instance._handleLayout(); + }); + }; + + window.addEventListener('resize', debounce(triggerAll, 16), false); + } } +const observe = instance => { + const id = guid(); + registry[id] = instance; + if (resizeObserver) { + const node = findNodeHandle(instance); + node._onLayoutId = id; + resizeObserver.observe(node); + } else { + instance._onLayoutId = id; + instance._handleLayout(); + } +}; + +const unobserve = instance => { + delete registry[instance._onLayoutId]; + if (resizeObserver) { + const node = findNodeHandle(instance); + delete node._onLayoutId; + resizeObserver.unobserve(node); + } else { + delete instance._onLayoutId; + } +}; + const safeOverride = (original, next) => { if (original) { return function prototypeOverride() { - original.call(this); - next.call(this); + /* eslint-disable prefer-rest-params */ + original.call(this, arguments); + next.call(this, arguments); + /* eslint-enable prefer-rest-params */ }; } return next; @@ -47,16 +92,20 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - this._onLayoutId = guid(); - registry[this._onLayoutId] = this; - this._handleLayout(); + observe(this); } ); Component.prototype.componentDidUpdate = safeOverride( componentDidUpdate, - function componentDidUpdate() { - this._handleLayout(); + function componentDidUpdate(prevProps) { + if (this.props.onLayout && !prevProps.onLayout) { + observe(this); + } else if (!this.props.onLayout && prevProps.onLayout) { + unobserve(this); + } else if (!resizeObserver) { + this._handleLayout(); + } } ); @@ -64,7 +113,7 @@ const applyLayout = Component => { componentWillUnmount, function componentWillUnmount() { this._isMounted = false; - delete registry[this._onLayoutId]; + unobserve(this); } ); diff --git a/website/guides/getting-started.md b/website/guides/getting-started.md index 5c5be3c34..4ca1cf595 100644 --- a/website/guides/getting-started.md +++ b/website/guides/getting-started.md @@ -2,8 +2,8 @@ This guide will help you to use and test React Native for Web once it has been installed. -Your application may need to polyfill `Promise`, `Object.assign`, and -`Array.from` as necessary for your desired browser support. +Your application may need to polyfill `Promise`, `Object.assign`, +`Array.from` and [`ResizeObserver`](#resizeobserve) as necessary for your desired browser support. ## Adding to a new web app @@ -272,3 +272,7 @@ flexbox layout. There are properties that do not work across all platforms. All web-specific props are annotated with `(web)` in the documentation. + +### ResizeObserver + +To observe layout changes and fire [`onLayout`](https://facebook.github.io/react-native/docs/view.html#onlayout) when elements are resized, React Native for Web uses the [`ResizeObserver` API](https://wicg.github.io/ResizeObserver/). Browsers that don't support this API require a [polyfill](https://github.com/que-etc/resize-observer-polyfill) for this feature. diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 7e275bd80..da43d37b3 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -4,6 +4,8 @@ * @flow */ +import { View } from 'react-native'; +import OnLayoutExample from './examples/onLayout'; import PropChildren from './examples/PropChildren'; import PropNumberOfLines from './examples/PropNumberOfLines'; import PropOnPress from './examples/PropOnPress'; @@ -132,13 +134,28 @@ const TextScreen = () => ( Invoked on mount and layout changes with{' '} {'{ nativeEvent: { layout: { x, y, width, height } } }'}, where{' '} x and y are the offsets from the parent node. + , + + NOTE: Behind the hood React Native for Web uses ResizeObserver and doesn't + polyfill it when not supported. - } + ]} + example={{ + render: () => ( + + + + + + + + ) + }} /> { + this.setState({ layoutInfo: nativeEvent.layout }); + }; + + render() { + return ( + + {JSON.stringify(this.state.layoutInfo)} + + ); + } +} + +const styles = StyleSheet.create({ + root: { + backgroundColor: '#eee' + } +}); diff --git a/website/storybook/1-components/View/ViewScreen.js b/website/storybook/1-components/View/ViewScreen.js index 5698c4886..ca9af43c2 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -4,6 +4,8 @@ * @flow */ +import { View } from 'react-native'; +import OnLayoutExample from './examples/onLayout'; import PropPointerEvents from './examples/PropPointerEvents'; import transformExamples from './examples/transforms'; import ZIndexExample from './examples/ZIndex'; @@ -125,13 +127,28 @@ const ViewScreen = () => ( Invoked on mount and layout changes with{' '} {'{ nativeEvent: { layout: { x, y, width, height } } }'}, where{' '} x and y are the offsets from the parent node. + , + + NOTE: Behind the hood React Native for Web uses ResizeObserver and doesn't + polyfill it when not supported. - } + ]} + example={{ + render: () => ( + + + + + + + + ) + }} /> { + this.setState({ layoutInfo: nativeEvent.layout }); + }; + + render() { + return ( + + {JSON.stringify(this.state.layoutInfo)} + + ); + } +} + +const styles = StyleSheet.create({ + root: { + backgroundColor: '#eee' + } +});