Skip to content

Commit

Permalink
[change] call 'onLayout' when elements are resized
Browse files Browse the repository at this point in the history
Uses ResizeObserver to monitor layout changes. Falls back to
window.onresize (with initial firing).

Fix #60
Close #848
  • Loading branch information
giuseppeg authored and necolas committed Apr 8, 2018
1 parent 9427eea commit 5a04d07
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 20 deletions.
79 changes: 65 additions & 14 deletions packages/react-native-web/src/modules/applyLayout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,76 @@

import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import debounce from 'debounce';
import findNodeHandle from '../../exports/findNodeHandle';

const emptyObject = {};
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' && process.env.NODE_ENV !== 'test') {
console.warn(
'onLayout relies on ResizeObserver which is not supported by your browser. ' +
'Please include a polyfill, e.g., https://github.com/que-etc/resize-observer-polyfill. ' +
'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 {
const id = guid();
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;
Expand All @@ -47,24 +94,28 @@ 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();
}
}
);

Component.prototype.componentWillUnmount = safeOverride(
componentWillUnmount,
function componentWillUnmount() {
this._isMounted = false;
delete registry[this._onLayoutId];
unobserve(this);
}
);

Expand Down
5 changes: 3 additions & 2 deletions website/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

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`](https://github.com/que-etc/resize-observer-polyfill) as
necessary for your desired browser support.

## Adding to a new web app

Expand Down
4 changes: 2 additions & 2 deletions website/storybook/1-components/Text/TextScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@ const TextScreen = () => (
<DocItem
name="onLayout"
typeInfo="?function"
description={
description={[
<AppText>
Invoked on mount and layout changes with{' '}
<Code>{'{ nativeEvent: { layout: { x, y, width, height } } }'}</Code>, where{' '}
<Code>x</Code> and <Code>y</Code> are the offsets from the parent node.
</AppText>
}
]}
/>

<DocItem
Expand Down
4 changes: 4 additions & 0 deletions website/storybook/1-components/View/ViewScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* @flow
*/

import PropOnLayout from './examples/PropOnLayout';
import PropPointerEvents from './examples/PropPointerEvents';
import transformExamples from './examples/transforms';
import ZIndexExample from './examples/ZIndex';
Expand Down Expand Up @@ -132,6 +133,9 @@ const ViewScreen = () => (
<Code>x</Code> and <Code>y</Code> are the offsets from the parent node.
</AppText>
}
example={{
render: () => <PropOnLayout />
}}
/>

<DocItem
Expand Down
76 changes: 76 additions & 0 deletions website/storybook/1-components/View/examples/PropOnLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @flow
*/

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

const l1 = { width: '100%', paddingLeft: 0, paddingTop: 0 };
const l2 = { width: '75%', paddingLeft: 10, paddingTop: 10 };

export default class ViewOnLayoutExample extends React.Component {
state = {
layoutInfo: {}
};

componentDidMount() {
this._layout = l1;

this._interval = setInterval(() => {
if (this._ref) {
this._ref.setNativeProps({ style: this._layout });
this._layout = this._layout.width === '100%' ? l2 : l1;
}
}, 2000);
}

componentWillUnmount() {
clearInterval(this._interval);
}

render() {
const { x, y, width, height } = this.state.layoutInfo;
return (
<View style={styles.root}>
<View style={styles.left}>
<Text>x: {x}</Text>
<Text>y: {y}</Text>
<Text>width: {width}</Text>
<Text>height: {height}</Text>
</View>
<View style={styles.right}>
<View ref={this._setRef} style={styles.container}>
<View onLayout={this._handleLayout} style={styles.box} />
</View>
</View>
</View>
);
}

_handleLayout = ({ nativeEvent }) => {
this.setState(() => ({ layoutInfo: nativeEvent.layout }));
};

_setRef = component => {
this._ref = component;
};
}

const styles = StyleSheet.create({
root: {
flexDirection: 'row'
},
container: {
height: 50
},
left: {
width: 100
},
right: {
flex: 1
},
box: {
backgroundColor: '#eee',
flex: 1
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Box = ({ pointerEvents }) => (
</TouchableHighlight>
);

const ViewStyleExample = () => (
const ViewPointerEventsExample = () => (
<View pointerEvents="box-none">
<View pointerEvents="box-none" style={styles.container}>
<Box pointerEvents="none" />
Expand All @@ -43,4 +43,4 @@ const styles = StyleSheet.create({
borderStyle: 'solid'
}
});
export default ViewStyleExample;
export default ViewPointerEventsExample;

0 comments on commit 5a04d07

Please sign in to comment.