From acf6e66806d82026c5bdb63788715b938d7a2666 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:31:25 +0100 Subject: [PATCH 01/10] Fire 'onLayout' when elements are resized --- .../src/modules/applyLayout/index.js | 115 +++++++++++++----- .../storybook/1-components/Text/TextScreen.js | 12 +- .../1-components/Text/examples/onLayout.js | 20 +++ .../storybook/1-components/View/ViewScreen.js | 12 +- .../1-components/View/examples/onLayout.js | 24 ++++ 5 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 website/storybook/1-components/Text/examples/onLayout.js create mode 100644 website/storybook/1-components/View/examples/onLayout.js diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index ecfbd6697..149388027 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,17 +17,70 @@ 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, contentRect }) => { + typeof target.handleResize === 'function' && + target.handleResize({ + x: contentRect.x, + y: contentRect.y, + width: contentRect.width, + height: contentRect.height + }); + }); }); - }; + } 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.' + ); + } + + const triggerAll = () => { + Object.keys(registry).forEach(key => { + const instance = registry[key]; + instance._isMounted && + instance.measure((x, y, width, height) => { + instance._handleLayout({ x, y, width, height }); + }); + }); + }; - window.addEventListener('resize', debounce(triggerAll, 16), false); + window.addEventListener('resize', debounce(triggerAll, 16), false); + } } +const observe = instance => { + const cb = instance._handleLayout.bind(instance); + + if (resizeObserver) { + const node = findNodeHandle(instance); + node.handleResize = debounce(cb); + resizeObserver.observe(node); + } else { + const id = guid(); + registry[id] = instance; + instance._onLayoutId = id; + instance.measure((x, y, width, height) => { + cb({ x, y, width, height }); + }); + } +}; + +const unobserve = instance => { + if (resizeObserver) { + const node = findNodeHandle(instance); + node.handleResize = null; + resizeObserver.unobserve(node); + } else { + delete registry[instance._onLayoutId]; + } +}; + const safeOverride = (original, next) => { if (original) { return function prototypeOverride() { @@ -47,16 +101,20 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - this._onLayoutId = guid(); - registry[this._onLayoutId] = this; - this._handleLayout(); + if (this.props.onLayout) { + 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); + } } ); @@ -64,29 +122,28 @@ const applyLayout = Component => { componentWillUnmount, function componentWillUnmount() { this._isMounted = false; - delete registry[this._onLayoutId]; + unobserve(this); } ); - Component.prototype._handleLayout = function() { - const layout = this._layoutState; + Component.prototype._handleLayout = function(layout) { const { onLayout } = this.props; - if (onLayout) { - this.measure((x, y, width, height) => { - if (!this._isMounted) return; - - if ( - layout.x !== x || - layout.y !== y || - layout.width !== width || - layout.height !== height - ) { - this._layoutState = { x, y, width, height }; - const nativeEvent = { layout: this._layoutState }; - onLayout({ nativeEvent, timeStamp: Date.now() }); - } - }); + if (typeof onLayout !== 'function' || !this._isMounted || !layout) { + return; + } + + const prevLayout = this._layoutState; + + if ( + prevLayout.x !== layout.x || + prevLayout.y !== layout.y || + prevLayout.width !== layout.width || + prevLayout.height !== layout.height + ) { + const nativeEvent = { layout }; + this._layoutState = layout; + onLayout({ nativeEvent, timeStamp: Date.now() }); } }; return Component; diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 7e275bd80..55534ecfc 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -4,6 +4,7 @@ * @flow */ +import OnLayoutExample from './examples/onLayout'; import PropChildren from './examples/PropChildren'; import PropNumberOfLines from './examples/PropNumberOfLines'; import PropOnPress from './examples/PropOnPress'; @@ -132,13 +133,20 @@ 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)}; + } +} diff --git a/website/storybook/1-components/View/ViewScreen.js b/website/storybook/1-components/View/ViewScreen.js index 5698c4886..4008af999 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -4,6 +4,7 @@ * @flow */ +import OnLayoutExample from './examples/onLayout'; import PropPointerEvents from './examples/PropPointerEvents'; import transformExamples from './examples/transforms'; import ZIndexExample from './examples/ZIndex'; @@ -125,13 +126,20 @@ 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)} + + ); + } +} From 74a405b2f3ac622419b49b504527fea385e045a9 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:34:48 +0100 Subject: [PATCH 02/10] s/handleResize/_handleLayout --- .../react-native-web/src/modules/applyLayout/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 149388027..1487485b7 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -22,8 +22,8 @@ if (canUseDOM) { if (typeof window.ResizeObserver !== 'undefined') { resizeObserver = new window.ResizeObserver(entries => { entries.forEach(({ target, contentRect }) => { - typeof target.handleResize === 'function' && - target.handleResize({ + typeof target._handleLayout === 'function' && + target._handleLayout({ x: contentRect.x, y: contentRect.y, width: contentRect.width, @@ -59,7 +59,7 @@ const observe = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node.handleResize = debounce(cb); + node._handleLayout = debounce(cb); resizeObserver.observe(node); } else { const id = guid(); @@ -74,7 +74,7 @@ const observe = instance => { const unobserve = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node.handleResize = null; + node._handleLayout = null; resizeObserver.unobserve(node); } else { delete registry[instance._onLayoutId]; From 9684c6d5c4933c58317b5b8ac6448dfe7da6e6a8 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:55:59 +0100 Subject: [PATCH 03/10] Invoke override with arguments --- packages/react-native-web/src/modules/applyLayout/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 1487485b7..9f7bffc1d 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -83,9 +83,9 @@ const unobserve = instance => { const safeOverride = (original, next) => { if (original) { - return function prototypeOverride() { - original.call(this); - next.call(this); + return function prototypeOverride(...args) { + original.apply(this, args); + next.apply(this, args); }; } return next; From 656a96a4f47abb3d21df7b788e64edff16d855f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Sun, 4 Mar 2018 12:10:05 +0100 Subject: [PATCH 04/10] Use RNW measure --- .../src/modules/applyLayout/index.js | 75 ++++++++----------- .../storybook/1-components/Text/TextScreen.js | 11 ++- .../1-components/Text/examples/onLayout.js | 18 ++++- .../storybook/1-components/View/ViewScreen.js | 11 ++- .../1-components/View/examples/onLayout.js | 14 +++- 5 files changed, 81 insertions(+), 48 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 9f7bffc1d..be573d067 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -21,14 +21,8 @@ let resizeObserver; if (canUseDOM) { if (typeof window.ResizeObserver !== 'undefined') { resizeObserver = new window.ResizeObserver(entries => { - entries.forEach(({ target, contentRect }) => { - typeof target._handleLayout === 'function' && - target._handleLayout({ - x: contentRect.x, - y: contentRect.y, - width: contentRect.width, - height: contentRect.height - }); + entries.forEach(({ target }) => { + target._handleLayout && target._handleLayout(); }); }); } else { @@ -43,10 +37,7 @@ if (canUseDOM) { const triggerAll = () => { Object.keys(registry).forEach(key => { const instance = registry[key]; - instance._isMounted && - instance.measure((x, y, width, height) => { - instance._handleLayout({ x, y, width, height }); - }); + instance._handleLayout(); }); }; @@ -55,37 +46,36 @@ if (canUseDOM) { } const observe = instance => { - const cb = instance._handleLayout.bind(instance); - if (resizeObserver) { const node = findNodeHandle(instance); - node._handleLayout = debounce(cb); + node._handleLayout = debounce(instance._handleLayout.bind(instance)); resizeObserver.observe(node); } else { const id = guid(); - registry[id] = instance; instance._onLayoutId = id; - instance.measure((x, y, width, height) => { - cb({ x, y, width, height }); - }); + registry[id] = instance; + instance._handleLayout(); } }; const unobserve = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node._handleLayout = null; + delete node._handleLayout; resizeObserver.unobserve(node); } else { delete registry[instance._onLayoutId]; + delete instance._onLayoutId; } }; const safeOverride = (original, next) => { if (original) { - return function prototypeOverride(...args) { - original.apply(this, args); - next.apply(this, args); + return function prototypeOverride() { + /* eslint-disable prefer-rest-params */ + original.call(this, arguments); + next.call(this, arguments); + /* eslint-enable prefer-rest-params */ }; } return next; @@ -101,9 +91,7 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - if (this.props.onLayout) { - observe(this); - } + observe(this); } ); @@ -114,6 +102,8 @@ const applyLayout = Component => { observe(this); } else if (!this.props.onLayout && prevProps.onLayout) { unobserve(this); + } else if (!resizeObserver) { + this._handleLayout(); } } ); @@ -126,24 +116,25 @@ const applyLayout = Component => { } ); - Component.prototype._handleLayout = function(layout) { + Component.prototype._handleLayout = function() { + const layout = this._layoutState; const { onLayout } = this.props; - if (typeof onLayout !== 'function' || !this._isMounted || !layout) { - return; - } - - const prevLayout = this._layoutState; - - if ( - prevLayout.x !== layout.x || - prevLayout.y !== layout.y || - prevLayout.width !== layout.width || - prevLayout.height !== layout.height - ) { - const nativeEvent = { layout }; - this._layoutState = layout; - onLayout({ nativeEvent, timeStamp: Date.now() }); + if (onLayout) { + this.measure((x, y, width, height) => { + if (!this._isMounted) return; + + if ( + layout.x !== x || + layout.y !== y || + layout.width !== width || + layout.height !== height + ) { + this._layoutState = { x, y, width, height }; + const nativeEvent = { layout: this._layoutState }; + onLayout({ nativeEvent, timeStamp: Date.now() }); + } + }); } }; return Component; diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 55534ecfc..da43d37b3 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -4,6 +4,7 @@ * @flow */ +import { View } from 'react-native'; import OnLayoutExample from './examples/onLayout'; import PropChildren from './examples/PropChildren'; import PropNumberOfLines from './examples/PropNumberOfLines'; @@ -145,7 +146,15 @@ const TextScreen = () => ( ]} example={{ - render: () => + render: () => ( + + + + + + + + ) }} /> diff --git a/website/storybook/1-components/Text/examples/onLayout.js b/website/storybook/1-components/Text/examples/onLayout.js index 9f226b7b1..b2765bfda 100644 --- a/website/storybook/1-components/Text/examples/onLayout.js +++ b/website/storybook/1-components/Text/examples/onLayout.js @@ -3,9 +3,13 @@ */ import React from 'react'; -import { Text } from 'react-native'; +import { Text, TextPropTypes, StyleSheet } from 'react-native'; export default class OnLayoutExample extends React.Component { + static propTypes = { + style: TextPropTypes.style + }; + state = { layoutInfo: {} }; @@ -15,6 +19,16 @@ export default class OnLayoutExample extends React.Component { }; render() { - return {JSON.stringify(this.state.layoutInfo)}; + 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 4008af999..ca9af43c2 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -4,6 +4,7 @@ * @flow */ +import { View } from 'react-native'; import OnLayoutExample from './examples/onLayout'; import PropPointerEvents from './examples/PropPointerEvents'; import transformExamples from './examples/transforms'; @@ -138,7 +139,15 @@ const ViewScreen = () => ( ]} example={{ - render: () => + render: () => ( + + + + + + + + ) }} /> diff --git a/website/storybook/1-components/View/examples/onLayout.js b/website/storybook/1-components/View/examples/onLayout.js index 9fd1a8e3d..a771dc13d 100644 --- a/website/storybook/1-components/View/examples/onLayout.js +++ b/website/storybook/1-components/View/examples/onLayout.js @@ -3,9 +3,13 @@ */ import React from 'react'; -import { Text, View } from 'react-native'; +import { Text, View, ViewPropTypes, StyleSheet } from 'react-native'; export default class OnLayoutExample extends React.Component { + static propTypes = { + style: ViewPropTypes.style + }; + state = { layoutInfo: {} }; @@ -16,9 +20,15 @@ export default class OnLayoutExample extends React.Component { render() { return ( - + {JSON.stringify(this.state.layoutInfo)} ); } } + +const styles = StyleSheet.create({ + root: { + backgroundColor: '#eee' + } +}); From c1810f94e08d2f1cf056cd3f195fa8aab129cc8b Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:31:25 +0100 Subject: [PATCH 05/10] Fire 'onLayout' when elements are resized --- .../src/modules/applyLayout/index.js | 115 +++++++++++++----- .../storybook/1-components/Text/TextScreen.js | 12 +- .../1-components/Text/examples/onLayout.js | 20 +++ .../storybook/1-components/View/ViewScreen.js | 12 +- .../1-components/View/examples/onLayout.js | 24 ++++ 5 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 website/storybook/1-components/Text/examples/onLayout.js create mode 100644 website/storybook/1-components/View/examples/onLayout.js diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index ecfbd6697..149388027 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,17 +17,70 @@ 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, contentRect }) => { + typeof target.handleResize === 'function' && + target.handleResize({ + x: contentRect.x, + y: contentRect.y, + width: contentRect.width, + height: contentRect.height + }); + }); }); - }; + } 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.' + ); + } + + const triggerAll = () => { + Object.keys(registry).forEach(key => { + const instance = registry[key]; + instance._isMounted && + instance.measure((x, y, width, height) => { + instance._handleLayout({ x, y, width, height }); + }); + }); + }; - window.addEventListener('resize', debounce(triggerAll, 16), false); + window.addEventListener('resize', debounce(triggerAll, 16), false); + } } +const observe = instance => { + const cb = instance._handleLayout.bind(instance); + + if (resizeObserver) { + const node = findNodeHandle(instance); + node.handleResize = debounce(cb); + resizeObserver.observe(node); + } else { + const id = guid(); + registry[id] = instance; + instance._onLayoutId = id; + instance.measure((x, y, width, height) => { + cb({ x, y, width, height }); + }); + } +}; + +const unobserve = instance => { + if (resizeObserver) { + const node = findNodeHandle(instance); + node.handleResize = null; + resizeObserver.unobserve(node); + } else { + delete registry[instance._onLayoutId]; + } +}; + const safeOverride = (original, next) => { if (original) { return function prototypeOverride() { @@ -47,16 +101,20 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - this._onLayoutId = guid(); - registry[this._onLayoutId] = this; - this._handleLayout(); + if (this.props.onLayout) { + 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); + } } ); @@ -64,29 +122,28 @@ const applyLayout = Component => { componentWillUnmount, function componentWillUnmount() { this._isMounted = false; - delete registry[this._onLayoutId]; + unobserve(this); } ); - Component.prototype._handleLayout = function() { - const layout = this._layoutState; + Component.prototype._handleLayout = function(layout) { const { onLayout } = this.props; - if (onLayout) { - this.measure((x, y, width, height) => { - if (!this._isMounted) return; - - if ( - layout.x !== x || - layout.y !== y || - layout.width !== width || - layout.height !== height - ) { - this._layoutState = { x, y, width, height }; - const nativeEvent = { layout: this._layoutState }; - onLayout({ nativeEvent, timeStamp: Date.now() }); - } - }); + if (typeof onLayout !== 'function' || !this._isMounted || !layout) { + return; + } + + const prevLayout = this._layoutState; + + if ( + prevLayout.x !== layout.x || + prevLayout.y !== layout.y || + prevLayout.width !== layout.width || + prevLayout.height !== layout.height + ) { + const nativeEvent = { layout }; + this._layoutState = layout; + onLayout({ nativeEvent, timeStamp: Date.now() }); } }; return Component; diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 7e275bd80..55534ecfc 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -4,6 +4,7 @@ * @flow */ +import OnLayoutExample from './examples/onLayout'; import PropChildren from './examples/PropChildren'; import PropNumberOfLines from './examples/PropNumberOfLines'; import PropOnPress from './examples/PropOnPress'; @@ -132,13 +133,20 @@ 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)}; + } +} diff --git a/website/storybook/1-components/View/ViewScreen.js b/website/storybook/1-components/View/ViewScreen.js index 5698c4886..4008af999 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -4,6 +4,7 @@ * @flow */ +import OnLayoutExample from './examples/onLayout'; import PropPointerEvents from './examples/PropPointerEvents'; import transformExamples from './examples/transforms'; import ZIndexExample from './examples/ZIndex'; @@ -125,13 +126,20 @@ 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)} + + ); + } +} From cb1437f5a38edd77737c44cf0bd181bd30140347 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:34:48 +0100 Subject: [PATCH 06/10] s/handleResize/_handleLayout --- .../react-native-web/src/modules/applyLayout/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 149388027..1487485b7 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -22,8 +22,8 @@ if (canUseDOM) { if (typeof window.ResizeObserver !== 'undefined') { resizeObserver = new window.ResizeObserver(entries => { entries.forEach(({ target, contentRect }) => { - typeof target.handleResize === 'function' && - target.handleResize({ + typeof target._handleLayout === 'function' && + target._handleLayout({ x: contentRect.x, y: contentRect.y, width: contentRect.width, @@ -59,7 +59,7 @@ const observe = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node.handleResize = debounce(cb); + node._handleLayout = debounce(cb); resizeObserver.observe(node); } else { const id = guid(); @@ -74,7 +74,7 @@ const observe = instance => { const unobserve = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node.handleResize = null; + node._handleLayout = null; resizeObserver.unobserve(node); } else { delete registry[instance._onLayoutId]; From db72e9b4d74e02b7dc5bf6d16abed24a0f93baa3 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:55:59 +0100 Subject: [PATCH 07/10] Invoke override with arguments --- packages/react-native-web/src/modules/applyLayout/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 1487485b7..9f7bffc1d 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -83,9 +83,9 @@ const unobserve = instance => { const safeOverride = (original, next) => { if (original) { - return function prototypeOverride() { - original.call(this); - next.call(this); + return function prototypeOverride(...args) { + original.apply(this, args); + next.apply(this, args); }; } return next; From 46772497c7bc0f0401f8822871c9b0c1338069f6 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Sun, 4 Mar 2018 12:10:05 +0100 Subject: [PATCH 08/10] Use RNW measure --- .../src/modules/applyLayout/index.js | 75 ++++++++----------- .../storybook/1-components/Text/TextScreen.js | 11 ++- .../1-components/Text/examples/onLayout.js | 18 ++++- .../storybook/1-components/View/ViewScreen.js | 11 ++- .../1-components/View/examples/onLayout.js | 14 +++- 5 files changed, 81 insertions(+), 48 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index 9f7bffc1d..be573d067 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -21,14 +21,8 @@ let resizeObserver; if (canUseDOM) { if (typeof window.ResizeObserver !== 'undefined') { resizeObserver = new window.ResizeObserver(entries => { - entries.forEach(({ target, contentRect }) => { - typeof target._handleLayout === 'function' && - target._handleLayout({ - x: contentRect.x, - y: contentRect.y, - width: contentRect.width, - height: contentRect.height - }); + entries.forEach(({ target }) => { + target._handleLayout && target._handleLayout(); }); }); } else { @@ -43,10 +37,7 @@ if (canUseDOM) { const triggerAll = () => { Object.keys(registry).forEach(key => { const instance = registry[key]; - instance._isMounted && - instance.measure((x, y, width, height) => { - instance._handleLayout({ x, y, width, height }); - }); + instance._handleLayout(); }); }; @@ -55,37 +46,36 @@ if (canUseDOM) { } const observe = instance => { - const cb = instance._handleLayout.bind(instance); - if (resizeObserver) { const node = findNodeHandle(instance); - node._handleLayout = debounce(cb); + node._handleLayout = debounce(instance._handleLayout.bind(instance)); resizeObserver.observe(node); } else { const id = guid(); - registry[id] = instance; instance._onLayoutId = id; - instance.measure((x, y, width, height) => { - cb({ x, y, width, height }); - }); + registry[id] = instance; + instance._handleLayout(); } }; const unobserve = instance => { if (resizeObserver) { const node = findNodeHandle(instance); - node._handleLayout = null; + delete node._handleLayout; resizeObserver.unobserve(node); } else { delete registry[instance._onLayoutId]; + delete instance._onLayoutId; } }; const safeOverride = (original, next) => { if (original) { - return function prototypeOverride(...args) { - original.apply(this, args); - next.apply(this, args); + return function prototypeOverride() { + /* eslint-disable prefer-rest-params */ + original.call(this, arguments); + next.call(this, arguments); + /* eslint-enable prefer-rest-params */ }; } return next; @@ -101,9 +91,7 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - if (this.props.onLayout) { - observe(this); - } + observe(this); } ); @@ -114,6 +102,8 @@ const applyLayout = Component => { observe(this); } else if (!this.props.onLayout && prevProps.onLayout) { unobserve(this); + } else if (!resizeObserver) { + this._handleLayout(); } } ); @@ -126,24 +116,25 @@ const applyLayout = Component => { } ); - Component.prototype._handleLayout = function(layout) { + Component.prototype._handleLayout = function() { + const layout = this._layoutState; const { onLayout } = this.props; - if (typeof onLayout !== 'function' || !this._isMounted || !layout) { - return; - } - - const prevLayout = this._layoutState; - - if ( - prevLayout.x !== layout.x || - prevLayout.y !== layout.y || - prevLayout.width !== layout.width || - prevLayout.height !== layout.height - ) { - const nativeEvent = { layout }; - this._layoutState = layout; - onLayout({ nativeEvent, timeStamp: Date.now() }); + if (onLayout) { + this.measure((x, y, width, height) => { + if (!this._isMounted) return; + + if ( + layout.x !== x || + layout.y !== y || + layout.width !== width || + layout.height !== height + ) { + this._layoutState = { x, y, width, height }; + const nativeEvent = { layout: this._layoutState }; + onLayout({ nativeEvent, timeStamp: Date.now() }); + } + }); } }; return Component; diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 55534ecfc..da43d37b3 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -4,6 +4,7 @@ * @flow */ +import { View } from 'react-native'; import OnLayoutExample from './examples/onLayout'; import PropChildren from './examples/PropChildren'; import PropNumberOfLines from './examples/PropNumberOfLines'; @@ -145,7 +146,15 @@ const TextScreen = () => ( ]} example={{ - render: () => + render: () => ( + + + + + + + + ) }} /> diff --git a/website/storybook/1-components/Text/examples/onLayout.js b/website/storybook/1-components/Text/examples/onLayout.js index 9f226b7b1..b2765bfda 100644 --- a/website/storybook/1-components/Text/examples/onLayout.js +++ b/website/storybook/1-components/Text/examples/onLayout.js @@ -3,9 +3,13 @@ */ import React from 'react'; -import { Text } from 'react-native'; +import { Text, TextPropTypes, StyleSheet } from 'react-native'; export default class OnLayoutExample extends React.Component { + static propTypes = { + style: TextPropTypes.style + }; + state = { layoutInfo: {} }; @@ -15,6 +19,16 @@ export default class OnLayoutExample extends React.Component { }; render() { - return {JSON.stringify(this.state.layoutInfo)}; + 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 4008af999..ca9af43c2 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -4,6 +4,7 @@ * @flow */ +import { View } from 'react-native'; import OnLayoutExample from './examples/onLayout'; import PropPointerEvents from './examples/PropPointerEvents'; import transformExamples from './examples/transforms'; @@ -138,7 +139,15 @@ const ViewScreen = () => ( ]} example={{ - render: () => + render: () => ( + + + + + + + + ) }} /> diff --git a/website/storybook/1-components/View/examples/onLayout.js b/website/storybook/1-components/View/examples/onLayout.js index 9fd1a8e3d..a771dc13d 100644 --- a/website/storybook/1-components/View/examples/onLayout.js +++ b/website/storybook/1-components/View/examples/onLayout.js @@ -3,9 +3,13 @@ */ import React from 'react'; -import { Text, View } from 'react-native'; +import { Text, View, ViewPropTypes, StyleSheet } from 'react-native'; export default class OnLayoutExample extends React.Component { + static propTypes = { + style: ViewPropTypes.style + }; + state = { layoutInfo: {} }; @@ -16,9 +20,15 @@ export default class OnLayoutExample extends React.Component { render() { return ( - + {JSON.stringify(this.state.layoutInfo)} ); } } + +const styles = StyleSheet.create({ + root: { + backgroundColor: '#eee' + } +}); From 9db10f12c4dd23c46adb3f33f86932fd57f1f33a Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Tue, 6 Mar 2018 19:47:11 +0100 Subject: [PATCH 09/10] Add note to the Getting started guide --- website/guides/getting-started.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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. From 5fd6a3c36aefaadd9fc26e28165223b63f1b19ab Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Wed, 4 Apr 2018 18:27:49 +0200 Subject: [PATCH 10/10] Hold ref to instance id rather than _handleLayout --- .../src/modules/applyLayout/index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index be573d067..e8e7944e3 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -22,7 +22,8 @@ if (canUseDOM) { if (typeof window.ResizeObserver !== 'undefined') { resizeObserver = new window.ResizeObserver(entries => { entries.forEach(({ target }) => { - target._handleLayout && target._handleLayout(); + const instance = registry[target._onLayoutId]; + instance && instance._handleLayout(); }); }); } else { @@ -46,25 +47,25 @@ if (canUseDOM) { } const observe = instance => { + const id = guid(); + registry[id] = instance; if (resizeObserver) { const node = findNodeHandle(instance); - node._handleLayout = debounce(instance._handleLayout.bind(instance)); + node._onLayoutId = id; resizeObserver.observe(node); } else { - const id = guid(); instance._onLayoutId = id; - registry[id] = instance; instance._handleLayout(); } }; const unobserve = instance => { + delete registry[instance._onLayoutId]; if (resizeObserver) { const node = findNodeHandle(instance); - delete node._handleLayout; + delete node._onLayoutId; resizeObserver.unobserve(node); } else { - delete registry[instance._onLayoutId]; delete instance._onLayoutId; } };