Oly
Oly

Reputation: 123

React Native auto height WebView doesn't work on android

I'm implementing a WebView with dynamic height. I found the solution that works like a charm on iOS and doesn't work on android. The solution uses JS inside the WV to set the title to the value of the content height. Here's the code:

...
this.state = {webViewHeight: 0};
...
<WebView
    source={{html: this.wrapWevViewHtml(this.state.content)}}
    style={{width: Dimensions.get('window').width - 20, height: this.state.webViewHeight}}
    scrollEnabled={false}
    javaScriptEnabled={true}
    injectedJavaScript="window.location.hash = 1;document.title = document.height;"
    onNavigationStateChange={this.onWebViewNavigationStateChange.bind(this)}
/>
...
onWebViewNavigationStateChange(navState) {
    // navState.title == height on iOS and html content on android
    if (navState.title) {
        this.setState({
            webViewHeight: Number(navState.title)
        });
    }
}
...

But on android the value of the title inside onWebViewNavigationStateChange is equal to page content.

What am I doing wrong?

Upvotes: 3

Views: 6668

Answers (2)

markbrown4
markbrown4

Reputation: 423

Loading a local HTML file on the device and injecting JS was the only method I found to correctly set the title / hash in Android.

/app/src/main/assets/blank.html

<!doctype html>
<html>
<head>
<title id="title">Go Web!</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
</head>
<body>
<div id="content"></div>
<script>
var content = document.getElementById('content');

var fireEvent = function(event, data) {
  document.title = data;
  window.location.hash = event;
};

var setContent = function(html) {
  content.innerHTML = html;
};
</script>
</body>
</html>

And the component

class ResizingWebView extends Component {
  constructor(props) {
    super(props)

    this.state = {
      height: 0
    }
  }
  onNavigationStateChange(navState) {
    var event = navState.url.split('#')[1]
    var data = navState.title

    console.log(event, data)
    if (event == 'resize') {
      this.setState({ height: data })
    }
  }
  render() {
    var scripts = "setContent('<h1>Yay!</h1>');fireEvent('resize', '300')";
    return (
      <WebView
        source={{ uri: 'file:///android_asset/blank.html' }}
        injectedJavaScript={ scripts }
        scalesPageToFit={ false }
        style={{ height: this.state.height }}
        onNavigationStateChange={ this.onNavigationStateChange.bind(this) }
      />
    )
  }
}

Upvotes: 0

esamatti
esamatti

Reputation: 18973

I was baffled by this too. It actually works but it's hard to debug why it does not work because Chrome remote debugging is not enabled for the React Native WebViews on Android.

I had two issues with this:

  1. The script I injected to the Webview contained some single line comments and on Android all the line breaks are removed (another bug?). It caused syntax errors in the WebView.

  2. On the first call the title content indeed is the full content of the Webview. No idea why but on latter calls it's the height. So just handle that case.

Here's the code I'm using now which on works on React Native 0.22 on Android and iOS

import React, {WebView, View, Text} from "react-native";


const BODY_TAG_PATTERN = /\<\/ *body\>/;

// Do not add any comments to this! It will break line breaks will removed for
// some weird reason.
var script = `
;(function() {
var wrapper = document.createElement("div");
wrapper.id = "height-wrapper";
while (document.body.firstChild) {
    wrapper.appendChild(document.body.firstChild);
}

document.body.appendChild(wrapper);

var i = 0;
function updateHeight() {
    document.title = wrapper.clientHeight;
    window.location.hash = ++i;
}
updateHeight();

window.addEventListener("load", function() {
    updateHeight();
    setTimeout(updateHeight, 1000);
});

window.addEventListener("resize", updateHeight);
}());
`;


const style = `
<style>
body, html, #height-wrapper {
    margin: 0;
    padding: 0;
}
#height-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
}
</style>
<script>
${script}
</script>
`;

const codeInject = (html) => html.replace(BODY_TAG_PATTERN, style + "</body>");


/**
 * Wrapped Webview which automatically sets the height according to the
 * content. Scrolling is always disabled. Required when the Webview is embedded
 * into a ScrollView with other components.
 *
 * Inspired by this SO answer http://stackoverflow.com/a/33012545
 * */
var WebViewAutoHeight = React.createClass({

    propTypes: {
        source: React.PropTypes.object.isRequired,
        injectedJavaScript: React.PropTypes.string,
        minHeight: React.PropTypes.number,
        onNavigationStateChange: React.PropTypes.func,
        style: WebView.propTypes.style,
    },

    getDefaultProps() {
        return {minHeight: 100};
    },

    getInitialState() {
        return {
            realContentHeight: this.props.minHeight,
        };
    },

    handleNavigationChange(navState) {
        if (navState.title) {
            const realContentHeight = parseInt(navState.title, 10) || 0; // turn NaN to 0
            this.setState({realContentHeight});
        }
        if (typeof this.props.onNavigationStateChange === "function") {
            this.props.onNavigationStateChange(navState);
        }
    },

    render() {
        const {source, style, minHeight, ...otherProps} = this.props;
        const html = source.html;

        if (!html) {
            throw new Error("WebViewAutoHeight supports only source.html");
        }

        if (!BODY_TAG_PATTERN.test(html)) {
            throw new Error("Cannot find </body> from: " + html);
        }

        return (
            <View>
                <WebView
                    {...otherProps}
                    source={{html: codeInject(html)}}
                    scrollEnabled={false}
                    style={[style, {height: Math.max(this.state.realContentHeight, minHeight)}]}
                    javaScriptEnabled
                    onNavigationStateChange={this.handleNavigationChange}
                />
                {process.env.NODE_ENV !== "production" &&
                <Text>Web content height: {this.state.realContentHeight}</Text>}
            </View>
        );
    },

});


export default WebViewAutoHeight;

As gist https://gist.github.com/epeli/10c77c1710dd137a1335

Upvotes: 5

Related Questions