diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index 711d1157d..b7108681e 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -40,6 +40,7 @@ var EXAMPLES = [
require('./AsyncStorageExample'),
require('./CameraRollExample.ios'),
require('./MapViewExample'),
+ require('./WebViewExample'),
require('./AppStateIOSExample'),
require('./AlertIOSExample'),
require('./AdSupportIOSExample'),
diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js
new file mode 100644
index 000000000..4b7a1513d
--- /dev/null
+++ b/Examples/UIExplorer/WebViewExample.js
@@ -0,0 +1,264 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ */
+'use strict';
+
+var React = require('react-native');
+var StyleSheet = require('StyleSheet');
+var {
+ ActivityIndicatorIOS,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+ WebView
+} = React;
+
+var HEADER = '#3b5998';
+var BGWASH = 'rgba(255,255,255,0.8)';
+var DISABLED_WASH = 'rgba(255,255,255,0.25)';
+
+var TEXT_INPUT_REF = 'urlInput';
+var WEBVIEW_REF = 'webview';
+var DEFAULT_URL = 'https://m.facebook.com';
+
+var WebViewExample = React.createClass({
+
+ getInitialState: function() {
+ return {
+ url: DEFAULT_URL,
+ status: 'No Page Loaded',
+ backButtonEnabled: false,
+ forwardButtonEnabled: false,
+ loading: true,
+ };
+ },
+
+ handleTextInputChange: function(event) {
+ this.inputText = event.nativeEvent.text;
+ },
+
+ render: function() {
+ this.inputText = this.state.url;
+
+ return (
+
+
+
+
+
+ {'<'}
+
+
+
+
+
+
+ {'>'}
+
+
+
+
+
+
+
+ Go!
+
+
+
+
+
+
+ {this.state.status}
+
+
+ );
+ },
+
+ goBack: function() {
+ this.refs[WEBVIEW_REF].goBack();
+ },
+
+ goForward: function() {
+ this.refs[WEBVIEW_REF].goForward();
+ },
+
+ reload: function() {
+ this.refs[WEBVIEW_REF].reload();
+ },
+
+ onNavigationStateChange: function(navState) {
+ this.setState({
+ backButtonEnabled: navState.canGoBack,
+ forwardButtonEnabled: navState.canGoForward,
+ url: navState.url,
+ status: navState.title,
+ loading: navState.loading,
+ });
+ },
+
+ renderErrorView: function(errorDomain, errorCode, errorDesc) {
+ return (
+
+
+ Error loading page
+
+
+ {'Domain: ' + errorDomain}
+
+
+ {'Error Code: ' + errorCode}
+
+
+ {'Description: ' + errorDesc}
+
+
+ );
+ },
+
+ renderLoadingView: function() {
+ return (
+
+
+
+ );
+ },
+
+ onSubmitEditing: function(event) {
+ this.pressGoButton();
+ },
+
+ pressGoButton: function() {
+ var url = this.inputText.toLowerCase();
+ if (url === this.state.url) {
+ this.reload();
+ } else {
+ this.setState({
+ url: url,
+ });
+ }
+ // dismiss keyoard
+ this.refs[TEXT_INPUT_REF].blur();
+ },
+
+});
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: HEADER,
+ },
+ addressBarRow: {
+ flexDirection: 'row',
+ padding: 8,
+ },
+ webView: {
+ backgroundColor: BGWASH,
+ height: 350,
+ },
+ addressBarTextInput: {
+ backgroundColor: BGWASH,
+ borderColor: 'transparent',
+ borderRadius: 3,
+ borderWidth: 1,
+ height: 24,
+ paddingLeft: 10,
+ paddingTop: 3,
+ paddingBottom: 3,
+ flex: 1,
+ fontSize: 14,
+ },
+ navButton: {
+ width: 20,
+ padding: 3,
+ marginRight: 3,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: BGWASH,
+ borderColor: 'transparent',
+ borderRadius: 3,
+ },
+ disabledButton: {
+ width: 20,
+ padding: 3,
+ marginRight: 3,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: DISABLED_WASH,
+ borderColor: 'transparent',
+ borderRadius: 3,
+ },
+ goButton: {
+ height: 24,
+ padding: 3,
+ marginLeft: 8,
+ alignItems: 'center',
+ backgroundColor: BGWASH,
+ borderColor: 'transparent',
+ borderRadius: 3,
+ alignSelf: 'stretch',
+ },
+ loadingView: {
+ backgroundColor: BGWASH,
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: BGWASH,
+ },
+ errorTextTitle: {
+ fontSize: 15,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ },
+ errorText: {
+ fontSize: 14,
+ textAlign: 'center',
+ marginBottom: 2,
+ },
+ statusBar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingLeft: 5,
+ height: 22,
+ },
+ statusBarText: {
+ color: 'white',
+ fontSize: 13,
+ },
+ spinner: {
+ width: 20,
+ marginRight: 6,
+ },
+});
+
+exports.title = '';
+exports.description = 'Base component to display web content';
+exports.examples = [
+ {
+ title: 'WebView',
+ render() { return ; }
+ }
+];
diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js
new file mode 100644
index 000000000..8190258ae
--- /dev/null
+++ b/Libraries/Components/WebView/WebView.android.js
@@ -0,0 +1,169 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule WebView
+ */
+'use strict';
+
+var EdgeInsetsPropType = require('EdgeInsetsPropType');
+var React = require('React');
+var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+
+var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass');
+var keyMirror = require('keyMirror');
+var merge = require('merge');
+
+var PropTypes = React.PropTypes;
+var RKUIManager = require('NativeModules').RKUIManager;
+
+var RK_WEBVIEW_REF = 'webview';
+
+var WebViewState = keyMirror({
+ IDLE: null,
+ LOADING: null,
+ ERROR: null,
+});
+
+var WebView = React.createClass({
+
+ propTypes: {
+ renderErrorView: PropTypes.func.isRequired, // view to show if there's an error
+ renderLoadingView: PropTypes.func.isRequired, // loading indicator to show
+ url: PropTypes.string.isRequired,
+ automaticallyAdjustContentInsets: PropTypes.bool,
+ contentInset: EdgeInsetsPropType,
+ onNavigationStateChange: PropTypes.func,
+ startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
+ style: View.propTypes.style,
+ /**
+ * Used to locate this view in end-to-end tests.
+ */
+ testID: PropTypes.string,
+ },
+
+ getInitialState: function() {
+ return {
+ viewState: WebViewState.IDLE,
+ lastErrorEvent: null,
+ startInLoadingState: true,
+ };
+ },
+
+ componentWillMount: function() {
+ if (this.props.startInLoadingState) {
+ this.setState({viewState: WebViewState.LOADING});
+ }
+ },
+
+ render: function() {
+ var otherView = null;
+
+ if (this.state.viewState === WebViewState.LOADING) {
+ otherView = this.props.renderLoadingView();
+ } else if (this.state.viewState === WebViewState.ERROR) {
+ var errorEvent = this.state.lastErrorEvent;
+ otherView = this.props.renderErrorView(
+ errorEvent.domain,
+ errorEvent.code,
+ errorEvent.description);
+ } else if (this.state.viewState !== WebViewState.IDLE) {
+ console.error("RCTWebView invalid state encountered: " + this.state.loading);
+ }
+
+ var webViewStyles = [styles.container, this.props.style];
+ if (this.state.viewState === WebViewState.LOADING ||
+ this.state.viewState === WebViewState.ERROR) {
+ // if we're in either LOADING or ERROR states, don't show the webView
+ webViewStyles.push(styles.hidden);
+ }
+
+ var webView =
+ ;
+
+ return (
+
+ {webView}
+ {otherView}
+
+ );
+ },
+
+ goForward: function() {
+ RKUIManager.webViewGoForward(this.getWebWiewHandle());
+ },
+
+ goBack: function() {
+ RKUIManager.webViewGoBack(this.getWebWiewHandle());
+ },
+
+ reload: function() {
+ RKUIManager.webViewReload(this.getWebWiewHandle());
+ },
+
+ /**
+ * We return an event with a bunch of fields including:
+ * url, title, loading, canGoBack, canGoForward
+ */
+ updateNavigationState: function(event) {
+ if (this.props.onNavigationStateChange) {
+ this.props.onNavigationStateChange(event.nativeEvent);
+ }
+ },
+
+ getWebWiewHandle: function() {
+ return this.refs[RK_WEBVIEW_REF].getNodeHandle();
+ },
+
+ onLoadingStart: function(event) {
+ this.updateNavigationState(event);
+ },
+
+ onLoadingError: function(event) {
+ event.persist(); // persist this event because we need to store it
+ console.error("encountered an error loading page", event.nativeEvent);
+
+ this.setState({
+ lastErrorEvent: event.nativeEvent,
+ viewState: WebViewState.ERROR
+ });
+ },
+
+ onLoadingFinish: function(event) {
+ this.setState({
+ viewState: WebViewState.IDLE,
+ });
+ this.updateNavigationState(event);
+ },
+});
+
+var RCTWebView = createReactIOSNativeComponentClass({
+ validAttributes: merge(ReactIOSViewAttributes.UIView, {
+ url: true,
+ }),
+ uiViewClassName: 'RCTWebView',
+});
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ hidden: {
+ height: 0,
+ flex: 0, // disable 'flex:1' when hiding a View
+ },
+});
+
+module.exports = WebView;
diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js
new file mode 100644
index 000000000..ff916f8e7
--- /dev/null
+++ b/Libraries/Components/WebView/WebView.ios.js
@@ -0,0 +1,182 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule WebView
+ */
+'use strict';
+
+var EdgeInsetsPropType = require('EdgeInsetsPropType');
+var React = require('React');
+var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+
+var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass');
+var keyMirror = require('keyMirror');
+var insetsDiffer = require('insetsDiffer');
+var merge = require('merge');
+
+var PropTypes = React.PropTypes;
+var { RKWebViewManager } = require('NativeModules');
+
+var RK_WEBVIEW_REF = 'webview';
+
+var WebViewState = keyMirror({
+ IDLE: null,
+ LOADING: null,
+ ERROR: null,
+});
+
+var NavigationType = {
+ click: RKWebViewManager.NavigationType.LinkClicked,
+ formsubmit: RKWebViewManager.NavigationType.FormSubmitted,
+ backforward: RKWebViewManager.NavigationType.BackForward,
+ reload: RKWebViewManager.NavigationType.Reload,
+ formresubmit: RKWebViewManager.NavigationType.FormResubmitted,
+ other: RKWebViewManager.NavigationType.Other,
+};
+
+var WebView = React.createClass({
+ statics: {
+ NavigationType: NavigationType,
+ },
+
+ propTypes: {
+ renderErrorView: PropTypes.func.isRequired, // view to show if there's an error
+ renderLoadingView: PropTypes.func.isRequired, // loading indicator to show
+ url: PropTypes.string.isRequired,
+ automaticallyAdjustContentInsets: PropTypes.bool,
+ shouldInjectAJAXHandler: PropTypes.bool,
+ contentInset: EdgeInsetsPropType,
+ onNavigationStateChange: PropTypes.func,
+ startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
+ style: View.propTypes.style,
+ },
+
+ getInitialState: function() {
+ return {
+ viewState: WebViewState.IDLE,
+ lastErrorEvent: null,
+ startInLoadingState: true,
+ };
+ },
+
+ componentWillMount: function() {
+ if (this.props.startInLoadingState) {
+ this.setState({viewState: WebViewState.LOADING});
+ }
+ },
+
+ render: function() {
+ var otherView = null;
+
+ if (this.state.viewState === WebViewState.LOADING) {
+ otherView = this.props.renderLoadingView();
+ } else if (this.state.viewState === WebViewState.ERROR) {
+ var errorEvent = this.state.lastErrorEvent;
+ otherView = this.props.renderErrorView(
+ errorEvent.domain,
+ errorEvent.code,
+ errorEvent.description);
+ } else if (this.state.viewState !== WebViewState.IDLE) {
+ console.error("RKWebView invalid state encountered: " + this.state.loading);
+ }
+
+ var webViewStyles = [styles.container, this.props.style];
+ if (this.state.viewState === WebViewState.LOADING ||
+ this.state.viewState === WebViewState.ERROR) {
+ // if we're in either LOADING or ERROR states, don't show the webView
+ webViewStyles.push(styles.hidden);
+ }
+
+ var webView =
+ ;
+
+ return (
+
+ {webView}
+ {otherView}
+
+ );
+ },
+
+ goForward: function() {
+ RKWebViewManager.goForward(this.getWebWiewHandle());
+ },
+
+ goBack: function() {
+ RKWebViewManager.goBack(this.getWebWiewHandle());
+ },
+
+ reload: function() {
+ RKWebViewManager.reload(this.getWebWiewHandle());
+ },
+
+ /**
+ * We return an event with a bunch of fields including:
+ * url, title, loading, canGoBack, canGoForward
+ */
+ updateNavigationState: function(event) {
+ if (this.props.onNavigationStateChange) {
+ this.props.onNavigationStateChange(event.nativeEvent);
+ }
+ },
+
+ getWebWiewHandle: function() {
+ return this.refs[RK_WEBVIEW_REF].getNodeHandle();
+ },
+
+ onLoadingStart: function(event) {
+ this.updateNavigationState(event);
+ },
+
+ onLoadingError: function(event) {
+ event.persist(); // persist this event because we need to store it
+ console.error("encountered an error loading page", event.nativeEvent);
+
+ this.setState({
+ lastErrorEvent: event.nativeEvent,
+ viewState: WebViewState.ERROR
+ });
+ },
+
+ onLoadingFinish: function(event) {
+ this.setState({
+ viewState: WebViewState.IDLE,
+ });
+ this.updateNavigationState(event);
+ },
+});
+
+var RCTWebView = createReactIOSNativeComponentClass({
+ validAttributes: merge(ReactIOSViewAttributes.UIView, {
+ url: true,
+ contentInset: {diff: insetsDiffer},
+ automaticallyAdjustContentInsets: true,
+ shouldInjectAJAXHandler: true
+ }),
+ uiViewClassName: 'RCTWebView',
+});
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ hidden: {
+ height: 0,
+ flex: 0, // disable 'flex:1' when hiding a View
+ },
+});
+
+module.exports = WebView;
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 5f4de8c1f..6f0b22df5 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -37,6 +37,7 @@ var ReactNative = {
TouchableOpacity: require('TouchableOpacity'),
TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
View: require('View'),
+ WebView: require('WebView'),
invariant: require('invariant'),
ix: require('ix'),
};
diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj
index eed3a13e7..d00c6b2c8 100644
--- a/ReactKit/ReactKit.xcodeproj/project.pbxproj
+++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj
@@ -31,6 +31,8 @@
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; };
13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; };
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; };
+ 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; };
+ 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; };
13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; };
13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; };
13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; };
@@ -124,6 +126,10 @@
13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; };
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; };
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; };
+ 13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; };
+ 13C156021AB1A2840079392D /* RCTWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebView.m; sourceTree = ""; };
+ 13C156031AB1A2840079392D /* RCTWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewManager.h; sourceTree = ""; };
+ 13C156041AB1A2840079392D /* RCTWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewManager.m; sourceTree = ""; };
13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; };
13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; };
13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; };
@@ -299,6 +305,10 @@
13E0674D1A70F44B002CDEE1 /* RCTViewManager.h */,
13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */,
13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */,
+ 13C156011AB1A2840079392D /* RCTWebView.h */,
+ 13C156021AB1A2840079392D /* RCTWebView.m */,
+ 13C156031AB1A2840079392D /* RCTWebViewManager.h */,
+ 13C156041AB1A2840079392D /* RCTWebViewManager.m */,
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */,
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */,
13E067531A70F44B002CDEE1 /* UIView+ReactKit.h */,
@@ -480,7 +490,9 @@
83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */,
14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */,
83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */,
+ 13C156051AB1A2840079392D /* RCTWebView.m in Sources */,
83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */,
+ 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */,
58114A161AAE854800E7D092 /* RCTPicker.m in Sources */,
137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */,
13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */,
diff --git a/ReactKit/Views/RCTWebView.h b/ReactKit/Views/RCTWebView.h
new file mode 100644
index 000000000..ec3c9d6eb
--- /dev/null
+++ b/ReactKit/Views/RCTWebView.h
@@ -0,0 +1,20 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTView.h"
+
+@class RCTEventDispatcher;
+
+@interface RCTWebView : RCTView
+
+@property (nonatomic, strong) NSURL *URL;
+@property (nonatomic, assign) UIEdgeInsets contentInset;
+@property (nonatomic, assign) BOOL shouldInjectAJAXHandler;
+@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
+
+- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
+
+- (void)goForward;
+- (void)goBack;
+- (void)reload;
+
+@end
diff --git a/ReactKit/Views/RCTWebView.m b/ReactKit/Views/RCTWebView.m
new file mode 100644
index 000000000..cc5a07578
--- /dev/null
+++ b/ReactKit/Views/RCTWebView.m
@@ -0,0 +1,180 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTWebView.h"
+
+#import
+
+#import "RCTAutoInsetsProtocol.h"
+#import "RCTEventDispatcher.h"
+#import "RCTLog.h"
+#import "RCTUtils.h"
+#import "RCTView.h"
+#import "UIView+ReactKit.h"
+
+@interface RCTWebView ()
+
+@end
+
+@implementation RCTWebView
+{
+ RCTEventDispatcher *_eventDispatcher;
+ UIWebView *_webView;
+}
+
+- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
+{
+ if ((self = [super initWithFrame:CGRectZero])) {
+ _automaticallyAdjustContentInsets = YES;
+ _contentInset = UIEdgeInsetsZero;
+ _eventDispatcher = eventDispatcher;
+ _webView = [[UIWebView alloc] initWithFrame:self.bounds];
+ _webView.delegate = self;
+ [self addSubview:_webView];
+ }
+ return self;
+}
+
+- (void)goForward
+{
+ [_webView goForward];
+}
+
+- (void)goBack
+{
+ [_webView goBack];
+}
+
+- (void)reload
+{
+ [_webView reload];
+}
+
+- (void)setURL:(NSURL *)URL
+{
+ // Because of the way React works, as pages redirect, we actually end up
+ // passing the redirect urls back here, so we ignore them if trying to load
+ // the same url. We'll expose a call to 'reload' to allow a user to load
+ // the existing page.
+ if ([URL isEqual:_webView.request.URL]) {
+ return;
+ }
+ if (!URL) {
+ // Clear the webview
+ [_webView loadHTMLString:nil baseURL:nil];
+ return;
+ }
+ [_webView loadRequest:[NSURLRequest requestWithURL:URL]];
+}
+
+- (void)layoutSubviews
+{
+ [super layoutSubviews];
+ _webView.frame = self.bounds;
+ [RCTView autoAdjustInsetsForView:self
+ withScrollView:_webView.scrollView
+ updateOffset:YES];
+}
+
+- (void)setContentInset:(UIEdgeInsets)contentInset
+{
+ _contentInset = contentInset;
+ [RCTView autoAdjustInsetsForView:self
+ withScrollView:_webView.scrollView
+ updateOffset:NO];
+}
+
+- (NSMutableDictionary *)baseEvent
+{
+ NSURL *url = _webView.request.URL;
+ NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"];
+ NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{
+ @"target": self.reactTag,
+ @"url": url ? [url absoluteString] : @"",
+ @"loading" : @(_webView.loading),
+ @"title": title,
+ @"canGoBack": @([_webView canGoBack]),
+ @"canGoForward" : @([_webView canGoForward]),
+ }];
+
+ return event;
+}
+
+#pragma mark - UIWebViewDelegate methods
+
+static NSString *const RCTJSAJAXScheme = @"react-ajax";
+
+- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
+ navigationType:(UIWebViewNavigationType)navigationType
+{
+ // We have this check to filter out iframe requests and whatnot
+ BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
+ if (isTopFrame) {
+ NSMutableDictionary *event = [self baseEvent];
+ [event addEntriesFromDictionary: @{
+ @"url": [request.URL absoluteString],
+ @"navigationType": @(navigationType)
+ }];
+ [_eventDispatcher sendInputEventWithName:@"topLoadingStart" body:event];
+ }
+
+ // AJAX handler
+ return ![request.URL.scheme isEqualToString:RCTJSAJAXScheme];
+}
+
+- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
+{
+ if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
+ // NSURLErrorCancelled is reported when a page has a redirect OR if you load
+ // a new URL in the WebView before the previous one came back. We can just
+ // ignore these since they aren't real errors.
+ // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
+ return;
+ }
+
+ NSMutableDictionary *event = [self baseEvent];
+ [event addEntriesFromDictionary: @{
+ @"domain": error.domain,
+ @"code": @(error.code),
+ @"description": [error localizedDescription],
+ }];
+ [_eventDispatcher sendInputEventWithName:@"topLoadingError" body:event];
+}
+
+- (void)webViewDidFinishLoad:(UIWebView *)webView
+{
+ if (_shouldInjectAJAXHandler) {
+
+ // From http://stackoverflow.com/questions/5353278/uiwebviewdelegate-not-monitoring-xmlhttprequest
+
+ [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"\
+ var s_ajaxListener = new Object(); \n\
+ s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open; \n\
+ s_ajaxListener.tempSend = XMLHttpRequest.prototype.send; \n\
+ s_ajaxListener.callback = function() { \n\
+ window.location.href = '%@://' + this.url; \n\
+ } \n\
+ XMLHttpRequest.prototype.open = function(a,b) { \n\
+ s_ajaxListener.tempOpen.apply(this, arguments); \n\
+ s_ajaxListener.method = a; \n\
+ s_ajaxListener.url = b; \n\
+ if (a.toLowerCase() === 'get') { \n\
+ s_ajaxListener.data = (b.split('?'))[1]; \n\
+ } \n\
+ } \n\
+ XMLHttpRequest.prototype.send = function(a,b) { \n\
+ s_ajaxListener.tempSend.apply(this, arguments); \n\
+ if (s_ajaxListener.method.toLowerCase() === 'post') { \n\
+ s_ajaxListener.data = a; \n\
+ } \n\
+ s_ajaxListener.callback(); \n\
+ } \n\
+ ", RCTJSAJAXScheme]];
+ }
+
+ // we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
+ if (!webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
+ [_eventDispatcher sendInputEventWithName:@"topLoadingFinish" body:[self baseEvent]];
+ }
+}
+
+@end
diff --git a/ReactKit/Views/RCTWebViewManager.h b/ReactKit/Views/RCTWebViewManager.h
new file mode 100644
index 000000000..d375cbdab
--- /dev/null
+++ b/ReactKit/Views/RCTWebViewManager.h
@@ -0,0 +1,7 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTViewManager.h"
+
+@interface RCTWebViewManager : RCTViewManager
+
+@end
diff --git a/ReactKit/Views/RCTWebViewManager.m b/ReactKit/Views/RCTWebViewManager.m
new file mode 100644
index 000000000..8e68d9eb8
--- /dev/null
+++ b/ReactKit/Views/RCTWebViewManager.m
@@ -0,0 +1,76 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTWebViewManager.h"
+
+#import "RCTBridge.h"
+#import "RCTSparseArray.h"
+#import "RCTUIManager.h"
+#import "RCTWebView.h"
+
+@implementation RCTWebViewManager
+
+- (UIView *)view
+{
+ return [[RCTWebView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
+}
+
+RCT_REMAP_VIEW_PROPERTY(url, URL);
+RCT_EXPORT_VIEW_PROPERTY(contentInset);
+RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets);
+RCT_EXPORT_VIEW_PROPERTY(shouldInjectAJAXHandler);
+
+- (NSDictionary *)constantsToExport
+{
+ return @{
+ @"NavigationType": @{
+ @"LinkClicked": @(UIWebViewNavigationTypeLinkClicked),
+ @"FormSubmitted": @(UIWebViewNavigationTypeFormSubmitted),
+ @"BackForward": @(UIWebViewNavigationTypeBackForward),
+ @"Reload": @(UIWebViewNavigationTypeReload),
+ @"FormResubmitted": @(UIWebViewNavigationTypeFormResubmitted),
+ @"Other": @(UIWebViewNavigationTypeOther)
+ },
+ };
+}
+
+- (void)goBack:(NSNumber *)reactTag
+{
+ RCT_EXPORT();
+
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ RCTWebView *view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[RCTWebView class]]) {
+ RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view);
+ }
+ [view goBack];
+ }];
+}
+
+- (void)goForward:(NSNumber *)reactTag
+{
+ RCT_EXPORT();
+
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ id view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[RCTWebView class]]) {
+ RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view);
+ }
+ [view goForward];
+ }];
+}
+
+
+- (void)reload:(NSNumber *)reactTag
+{
+ RCT_EXPORT();
+
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ RCTWebView *view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[RCTWebView class]]) {
+ RCTLogMustFix(@"Invalid view returned from registry, expecting RKWebView, got: %@", view);
+ }
+ [view reload];
+ }];
+}
+
+@end