From c61df66902c277f4e123263864cd324229b69fc4 Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Tue, 15 Nov 2016 11:12:13 +0100 Subject: [PATCH 1/7] Add props for overriding native component --- Libraries/Components/WebView/WebView.android.js | 17 ++++++++++++++++- Libraries/Components/WebView/WebView.ios.js | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 4617b2cb6a73b5..e221dd6eb77c78 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -160,6 +160,18 @@ class WebView extends React.Component { * @platform android */ allowUniversalAccessFromFileURLs: PropTypes.bool, + + /** + * Override the native component used to render the WebView. Enables a custom native + * WebView which uses the same JavaScript as the original WebView. + */ + nativeComponent: PropTypes.any, + + /** + * Set props directly on the native component WebView. Enables custom props which the + * original WebView doesn't pass through. + */ + nativeComponentProps: PropTypes.object }; static defaultProps = { @@ -214,8 +226,11 @@ class WebView extends React.Component { console.warn('WebView: `source.body` is not supported when using GET.'); } + let NativeWebView = this.props.nativeComponent || RCTWebView; + var webView = - Date: Tue, 15 Nov 2016 12:33:54 +0100 Subject: [PATCH 2/7] Fix trailing white space --- Libraries/Components/WebView/WebView.android.js | 2 +- Libraries/Components/WebView/WebView.ios.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index e221dd6eb77c78..94a9991c3a3ba6 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -162,7 +162,7 @@ class WebView extends React.Component { allowUniversalAccessFromFileURLs: PropTypes.bool, /** - * Override the native component used to render the WebView. Enables a custom native + * Override the native component used to render the WebView. Enables a custom native * WebView which uses the same JavaScript as the original WebView. */ nativeComponent: PropTypes.any, diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index d2355cf59760c5..d8616d8f207f99 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -336,7 +336,7 @@ class WebView extends React.Component { mediaPlaybackRequiresUserAction: PropTypes.bool, /** - * Override the native component used to render the WebView. Enables a custom native + * Override the native component used to render the WebView. Enables a custom native * WebView which uses the same JavaScript as the original WebView. */ nativeComponent: PropTypes.any, From 6231b5c7e18c8d119d95d7c340de82d4cb0aec58 Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Mon, 15 May 2017 14:41:57 +0200 Subject: [PATCH 3/7] Loop through RCTViewManager inheritance tree Allows for easier extension of ViewManagers --- React/Views/RCTComponentData.m | 74 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index 91c92f94a60ffd..d11f5a2c84ca3d 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -383,6 +383,7 @@ - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowV - (NSDictionary *)viewConfig { + NSMutableDictionary *propTypes = [NSMutableDictionary new]; NSMutableArray *bubblingEvents = [NSMutableArray new]; NSMutableArray *directEvents = [NSMutableArray new]; @@ -395,42 +396,47 @@ - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowV } } #pragma clang diagnostic pop - - unsigned int count = 0; - NSMutableDictionary *propTypes = [NSMutableDictionary new]; - Method *methods = class_copyMethodList(object_getClass(_managerClass), &count); - for (unsigned int i = 0; i < count; i++) { - SEL selector = method_getName(methods[i]); - const char *selectorName = sel_getName(selector); - if (strncmp(selectorName, "propConfig", strlen("propConfig")) != 0) { - continue; - } - - // We need to handle both propConfig_* and propConfigShadow_* methods - const char *underscorePos = strchr(selectorName + strlen("propConfig"), '_'); - if (!underscorePos) { - continue; - } - - NSString *name = @(underscorePos + 1); - NSString *type = ((NSArray *(*)(id, SEL))objc_msgSend)(_managerClass, selector)[0]; - if (RCT_DEBUG && propTypes[name] && ![propTypes[name] isEqualToString:type]) { - RCTLogError(@"Property '%@' of component '%@' redefined from '%@' " - "to '%@'", name, _name, propTypes[name], type); - } - - if ([type isEqualToString:@"RCTBubblingEventBlock"]) { - [bubblingEvents addObject:RCTNormalizeInputEventName(name)]; - propTypes[name] = @"BOOL"; - } else if ([type isEqualToString:@"RCTDirectEventBlock"]) { - [directEvents addObject:RCTNormalizeInputEventName(name)]; - propTypes[name] = @"BOOL"; - } else { - propTypes[name] = type; + + Class superClass = _managerClass; + while (superClass && superClass != [RCTViewManager class]) { + if ([superClass isSubclassOfClass:[RCTViewManager class]]) { + unsigned int count = 0; + Method *methods = class_copyMethodList(object_getClass(superClass), &count); + for (unsigned int i = 0; i < count; i++) { + SEL selector = method_getName(methods[i]); + const char *selectorName = sel_getName(selector); + if (strncmp(selectorName, "propConfig", strlen("propConfig")) != 0) { + continue; + } + + // We need to handle both propConfig_* and propConfigShadow_* methods + const char *underscorePos = strchr(selectorName + strlen("propConfig"), '_'); + if (!underscorePos) { + continue; + } + + NSString *name = @(underscorePos + 1); + NSString *type = ((NSArray *(*)(id, SEL))objc_msgSend)(superClass, selector)[0]; + if (RCT_DEBUG && propTypes[name] && ![propTypes[name] isEqualToString:type]) { + RCTLogError(@"Property '%@' of component '%@' redefined from '%@' " + "to '%@'", name, _name, propTypes[name], type); + } + + if ([type isEqualToString:@"RCTBubblingEventBlock"]) { + [bubblingEvents addObject:RCTNormalizeInputEventName(name)]; + propTypes[name] = @"BOOL"; + } else if ([type isEqualToString:@"RCTDirectEventBlock"]) { + [directEvents addObject:RCTNormalizeInputEventName(name)]; + propTypes[name] = @"BOOL"; + } else { + propTypes[name] = type; + } + } + free(methods); } + superClass = [superClass superclass]; } - free(methods); - + #if RCT_DEBUG for (NSString *event in bubblingEvents) { if ([directEvents containsObject:event]) { From 402560f577a9f889993fb44c12922f23d4ea0abc Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Tue, 16 May 2017 07:18:34 +0200 Subject: [PATCH 4/7] Export extra native component config for WebView --- .../Components/WebView/WebView.android.js | 14 ++++++++----- Libraries/Components/WebView/WebView.ios.js | 21 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 9954945e943dc3..8879b80ec4fc9a 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -45,6 +45,14 @@ var defaultRenderLoading = () => ( * Renders a native WebView. */ class WebView extends React.Component { + static get extraNativeComponentConfig() { + return { + nativeOnly: { + messagingEnabled: PropTypes.bool, + }, + }; + } + static propTypes = { ...ViewPropTypes, renderError: PropTypes.func, @@ -395,11 +403,7 @@ class WebView extends React.Component { } } -var RCTWebView = requireNativeComponent('RCTWebView', WebView, { - nativeOnly: { - messagingEnabled: PropTypes.bool, - }, -}); +var RCTWebView = requireNativeComponent('RCTWebView', WebView, WebView.extraNativeComponentConfig); var styles = StyleSheet.create({ container: { diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 0b3636e83f4637..536e9488208c1c 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -116,6 +116,17 @@ var defaultRenderError = (errorDomain, errorCode, errorDesc) => ( class WebView extends React.Component { static JSNavigationScheme = JSNavigationScheme; static NavigationType = NavigationType; + static get extraNativeComponentConfig() { + return { + nativeOnly: { + onLoadingStart: true, + onLoadingError: true, + onLoadingFinish: true, + onMessage: true, + messagingEnabled: PropTypes.bool, + }, + }; + } static propTypes = { ...ViewPropTypes, @@ -593,15 +604,7 @@ class WebView extends React.Component { } } -var RCTWebView = requireNativeComponent('RCTWebView', WebView, { - nativeOnly: { - onLoadingStart: true, - onLoadingError: true, - onLoadingFinish: true, - onMessage: true, - messagingEnabled: PropTypes.bool, - }, -}); +var RCTWebView = requireNativeComponent('RCTWebView', WebView, WebView.extraNativeComponentConfig); var styles = StyleSheet.create({ container: { From 2f8e56e2aee84640cc7b2d2e5735222f7bb26e33 Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Tue, 16 May 2017 07:30:05 +0200 Subject: [PATCH 5/7] Fix trailing spaces --- Libraries/Components/WebView/WebView.android.js | 2 +- Libraries/Components/WebView/WebView.ios.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 8879b80ec4fc9a..9fd141d8628d24 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -45,7 +45,7 @@ var defaultRenderLoading = () => ( * Renders a native WebView. */ class WebView extends React.Component { - static get extraNativeComponentConfig() { + static get extraNativeComponentConfig() { return { nativeOnly: { messagingEnabled: PropTypes.bool, diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 536e9488208c1c..2d15c336cd5067 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -116,7 +116,7 @@ var defaultRenderError = (errorDomain, errorCode, errorDesc) => ( class WebView extends React.Component { static JSNavigationScheme = JSNavigationScheme; static NavigationType = NavigationType; - static get extraNativeComponentConfig() { + static get extraNativeComponentConfig() { return { nativeOnly: { onLoadingStart: true, From d7b360efd527b91666a2e13319d7240782bedd4a Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Tue, 16 May 2017 12:41:59 +0200 Subject: [PATCH 6/7] Set methods/fields as protected instead of private For easier extension/overriding of behavior, and less duplication of code --- .../views/webview/ReactWebViewManager.java | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 7e246a7ff68f47..8946309a99f6ed 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -86,11 +86,11 @@ public class ReactWebViewManager extends SimpleViewManager { protected static final String REACT_CLASS = "RCTWebView"; - private static final String HTML_ENCODING = "UTF-8"; - private static final String HTML_MIME_TYPE = "text/html; charset=utf-8"; - private static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; + protected static final String HTML_ENCODING = "UTF-8"; + protected static final String HTML_MIME_TYPE = "text/html; charset=utf-8"; + protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; - private static final String HTTP_METHOD_POST = "POST"; + protected static final String HTTP_METHOD_POST = "POST"; public static final int COMMAND_GO_BACK = 1; public static final int COMMAND_GO_FORWARD = 2; @@ -101,14 +101,14 @@ public class ReactWebViewManager extends SimpleViewManager { // Use `webView.loadUrl("about:blank")` to reliably reset the view // state and release page resources (including any running JavaScript). - private static final String BLANK_URL = "about:blank"; + protected static final String BLANK_URL = "about:blank"; - private WebViewConfig mWebViewConfig; - private @Nullable WebView.PictureListener mPictureListener; + protected WebViewConfig mWebViewConfig; + protected @Nullable WebView.PictureListener mPictureListener; protected static class ReactWebViewClient extends WebViewClient { - private boolean mLastLoadFailed = false; + protected boolean mLastLoadFailed = false; @Override public void onPageFinished(WebView webView, String url) { @@ -184,7 +184,7 @@ public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload createWebViewEvent(webView, url))); } - private void emitFinishEvent(WebView webView, String url) { + protected void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, new TopLoadingFinishEvent( @@ -192,7 +192,7 @@ private void emitFinishEvent(WebView webView, String url) { createWebViewEvent(webView, url))); } - private WritableMap createWebViewEvent(WebView webView, String url) { + protected WritableMap createWebViewEvent(WebView webView, String url) { WritableMap event = Arguments.createMap(); event.putDouble("target", webView.getId()); // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks @@ -211,10 +211,10 @@ private WritableMap createWebViewEvent(WebView webView, String url) { * to call {@link WebView#destroy} on activty destroy event and also to clear the client */ protected static class ReactWebView extends WebView implements LifecycleEventListener { - private @Nullable String injectedJS; - private boolean messagingEnabled = false; + protected @Nullable String injectedJS; + protected boolean messagingEnabled = false; - private class ReactWebViewBridge { + protected class ReactWebViewBridge { ReactWebView mContext; ReactWebViewBridge(ReactWebView c) { @@ -257,6 +257,10 @@ public void setInjectedJavaScript(@Nullable String js) { injectedJS = js; } + protected ReactWebViewBridge createReactWebViewBridge(ReactWebView webView) { + return new ReactWebViewBridge(webView); + } + public void setMessagingEnabled(boolean enabled) { if (messagingEnabled == enabled) { return; @@ -264,7 +268,7 @@ public void setMessagingEnabled(boolean enabled) { messagingEnabled = enabled; if (enabled) { - addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME); + addJavascriptInterface(createReactWebViewBridge(this), BRIDGE_NAME); linkBridge(); } else { removeJavascriptInterface(BRIDGE_NAME); @@ -307,7 +311,7 @@ public void onMessage(String message) { dispatchEvent(this, new TopMessageEvent(this.getId(), message)); } - private void cleanupCallbacksAndDestroy() { + protected void cleanupCallbacksAndDestroy() { setWebViewClient(null); destroy(); } @@ -329,9 +333,13 @@ public String getName() { return REACT_CLASS; } + protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContext) { + return new ReactWebView(reactContext); + } + @Override protected WebView createViewInstance(ThemedReactContext reactContext) { - ReactWebView webView = new ReactWebView(reactContext); + ReactWebView webView = createReactWebViewInstance(reactContext); webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onConsoleMessage(ConsoleMessage message) { @@ -560,7 +568,7 @@ public void onDropViewInstance(WebView webView) { ((ReactWebView) webView).cleanupCallbacksAndDestroy(); } - private WebView.PictureListener getPictureListener() { + protected WebView.PictureListener getPictureListener() { if (mPictureListener == null) { mPictureListener = new WebView.PictureListener() { @Override @@ -577,7 +585,7 @@ public void onNewPicture(WebView webView, Picture picture) { return mPictureListener; } - private static void dispatchEvent(WebView webView, Event event) { + protected static void dispatchEvent(WebView webView, Event event) { ReactContext reactContext = (ReactContext) webView.getContext(); EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); From ad98f8556d1d3685a613fc4ff2806e665936fe0c Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Wed, 17 May 2017 08:57:33 +0100 Subject: [PATCH 7/7] Add docs for custom web views --- docs/CustomWebViewAndroid.md | 265 ++++++++++++++++++++++++++++++++ docs/CustomWebViewIOS.md | 227 +++++++++++++++++++++++++++ docs/HeadlessJSAndroid.md | 2 +- docs/LinkingLibraries.md | 2 +- docs/NativeComponentsAndroid.md | 2 +- docs/NativeComponentsIOS.md | 2 +- 6 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 docs/CustomWebViewAndroid.md create mode 100644 docs/CustomWebViewIOS.md diff --git a/docs/CustomWebViewAndroid.md b/docs/CustomWebViewAndroid.md new file mode 100644 index 00000000000000..1e10e018db1d6f --- /dev/null +++ b/docs/CustomWebViewAndroid.md @@ -0,0 +1,265 @@ +--- +id: custom-webview-android +title: Custom WebView +layout: docs +category: Guides (Android) +permalink: docs/custom-webview-android.html +banner: ejected +next: headless-js-android +previous: native-components-android +--- + +While the built-in web view has a lot of features, it is not possible to handle every use-case in React Native. You can, however, extend the web view with native code without forking React Native or duplicating all the existing web view code. + +Before you do this, you should be familiar with the concepts in [native UI components](native-components-android). You should also familiarise yourself with the [native code for web views](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java), as you will have to use this as a reference when implementing new features—although a deep understanding is not required. + +## Native Code + +To get started, you'll need to create a subclass of `ReactWebViewManager`, `ReactWebView`, and `ReactWebViewClient`. In your view manager, you'll then need to override: + +* `createReactWebViewInstance` +* `getName` +* `addEventEmitters` + +```java +@ReactModule(name = CustomWebViewManager.REACT_CLASS) +public class CustomWebViewManager extends ReactWebViewManager { + /* This name must match what we're referring to in JS */ + protected static final String REACT_CLASS = "RCTCustomWebView"; + + protected static class CustomWebViewClient extends ReactWebViewClient { } + + protected static class CustomWebView extends ReactWebView { + public CustomWebView(ThemedReactContext reactContext) { + super(reactContext); + } + } + + @Override + protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContext) { + return new CustomWebView(reactContext); + } + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { + view.setWebViewClient(new CustomWebViewClient()); + } +} +``` + +You'll need to follow the usual steps to [register the module](docs/native-modules-android.html#register-the-module). + +### Adding New Properties + +To add a new property, you'll need to add it to `CustomWebView`, and then expose it in `CustomWebViewManager`. + +```java +public class CustomWebViewManager extends ReactWebViewManager { + ... + + protected static class CustomWebView extends ReactWebView { + public CustomWebView(ThemedReactContext reactContext) { + super(reactContext); + } + + protected @Nullable String mFinalUrl; + + public void setFinalUrl(String url) { + mFinalUrl = url; + } + + public String getFinalUrl() { + return mFinalUrl; + } + } + + ... + + @ReactProp(name = "finalUrl") + public void setFinalUrl(WebView view, String url) { + ((CustomWebView) view).setFinalUrl(url); + } +} +``` + +### Adding New Events + +For events, you'll first need to make create event subclass. + +```java +// NavigationCompletedEvent.java +public class NavigationCompletedEvent extends Event { + private WritableMap mParams; + + public NavigationCompletedEvent(int viewTag, WritableMap params) { + super(viewTag); + this.mParams = params; + } + + @Override + public String getEventName() { + return "navigationCompleted"; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + init(getViewTag()); + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mParams); + } +} +``` + +You can trigger the event in your web view client. You can hook existing handlers if your events are based on them. + +You should refer to [ReactWebViewManager.java](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java) in the React Native codebase to see what handlers are available and how they are implemented. You can extend any methods here to provide extra functionality. + +```java +public class NavigationCompletedEvent extends Event { + private WritableMap mParams; + + public NavigationCompletedEvent(int viewTag, WritableMap params) { + super(viewTag); + this.mParams = params; + } + + @Override + public String getEventName() { + return "navigationCompleted"; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + init(getViewTag()); + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mParams); + } +} + +// CustomWebViewManager.java +protected static class CustomWebViewClient extends ReactWebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + boolean allowed = super.shouldOverrideUrlLoading(view, url); + String finalUrl = ((CustomWebView) view).getFinalUrl(); + + if (allowed && url != null && finalUrl != null && Object.equals(url, finalUrl)) { + final WritableMap params = Arguments.createMap(); + dispatchEvent(view, new NavigationCompletedEvent(view.getId(), params)); + } + + return allowed; + } +} +``` + +Finally, you'll need to expose the events in `CustomWebViewManager` through `getExportedCustomDirectEventTypeConstants`. Note that currently, the default implementation returns `null`, but this may change in the future. + +```java +public class CustomWebViewManager extends ReactWebViewManager { + ... + + @Override + public @Nullable + Map getExportedCustomDirectEventTypeConstants() { + Map export = super.getExportedCustomDirectEventTypeConstants(); + if (export == null) { + export = MapBuilder.newHashMap(); + } + export.put("navigationCompleted", MapBuilder.of("registrationName", "onNavigationCompleted")); + return export; + } +} +``` + +## JavaScript Interface + +To use your custom web view, you'll need to create a class for it. Your class must: + +* Export all the prop types from `WebView.propTypes` +* Return a `WebView` component with the prop `nativeComponent` set to your native component (see below) + +To get your native component, you must use `requireNativeComponent`: the same as for regular custom components. However, you must pass in an extra third argument, `WebView.extraNativeComponentConfig`. This third argument contains prop types that are only required for native code. + +```js +import { Component } from 'react'; +import { WebView, requireNativeComponent } from 'react-native'; + +export default class CustomWebView extends Component { + static propTypes = WebView.propTypes + + render() { + return ( + + ); + } +} + +const RCTCustomWebView = requireNativeComponent( + 'RCTCustomWebView', + CustomWebView, + WebView.extraNativeComponentConfig +); +``` + +If you want to add custom props to your native component, you can use `nativeComponentProps` on the web view. + +For events, the event handler must always be set to a function. This means it isn't safe to use the event handler directly from `this.props`, as the user might not have provided one. The standard approach is to create a event handler in your class, and then invoking the event handler given in `this.props` if it exists. + +If you are unsure how something should be implemented from the JS side, look at [WebView.android.js](https://github.com/facebook/react-native/blob/master/Libraries/Components/WebView/WebView.android.js) in the React Native source. + +```js +export default class CustomWebView extends Component { + static propTypes = { + ...WebView.propTypes, + finalUrl: PropTypes.string, + onNavigationCompleted: PropTypes.func, + }; + + static defaultProps = { + finalUrl: 'about:blank', + }; + + _onNavigationCompleted = (event) => { + const { onNavigationCompleted } = this.props; + onNavigationCompleted && onNavigationCompleted(event); + } + + render() { + return ( + + ); + } +} +``` + +Just like for regular native components, you must provide all your prop types in the component to have them forwarded on to the native component. However, if you have some prop types that are only used internally in component, you can add them to the `nativeOnly` property of the third argument previously mentioned. For event handlers, you have to use the value `true` instead of a regular prop type. + +For example, if you wanted to add an internal event handler called `onScrollToBottom`, you would use, + +```js +const RCTCustomWebView = requireNativeComponent( + 'RCTCustomWebView', + CustomWebView, + { + ...WebView.extraNativeComponentConfig, + nativeOnly: { + ...WebView.extraNativeComponentConfig.nativeOnly, + onScrollToBottom: true, + }, + } +); +``` diff --git a/docs/CustomWebViewIOS.md b/docs/CustomWebViewIOS.md new file mode 100644 index 00000000000000..5d21055967a97d --- /dev/null +++ b/docs/CustomWebViewIOS.md @@ -0,0 +1,227 @@ +--- +id: custom-webview-ios +title: Custom WebView +layout: docs +category: Guides (iOS) +permalink: docs/custom-webview-ios.html +banner: ejected +next: linking-libraries-ios +previous: native-components-ios +--- + +While the built-in web view has a lot of features, it is not possible to handle every use-case in React Native. You can, however, extend the web view with native code without forking React Native or duplicating all the existing web view code. + +Before you do this, you should be familiar with the concepts in [native UI components](native-components-ios). You should also familiarise yourself with the [native code for web views](https://github.com/facebook/react-native/blob/master/React/Views/RCTWebViewManager.m), as you will have to use this as a reference when implementing new features—although a deep understanding is not required. + +## Native Code + +Like for regular native components, you need a view manager and an web view. + +For the view, you'll need to make a subclass of `RCTWebView`. + +```objc +// RCTCustomWebView.h +#import + +@interface RCTCustomWebView : RCTWebView + +@end + +// RCTCustomWebView.m +#import "RCTCustomWebView.h" + +@interface RCTCustomWebView () + +@end + +@implementation RCTCustomWebView { } + +@end +``` + +For the view manager, you need to make a subclass `RCTWebViewManager`. You must still include: + +* `(UIView *)view` that returns your custom view +* The `RCT_EXPORT_MODULE()` tag + +```objc +// RCTCustomWebViewManager.h +#import + +@interface RCTCustomWebViewManager : RCTWebViewManager + +@end + +// RCTCustomWebViewManager.m +#import "RCTCustomWebViewManager.h" +#import "RCTCustomWebView.h" + +@interface RCTCustomWebViewManager () + +@end + +@implementation RCTCustomWebViewManager { } + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + RCTCustomWebView *webView = [RCTCustomWebView new]; + webView.delegate = self; + return webView; +} + +@end +``` + +### Adding New Events and Properties + +Adding new properties and events is the same as regular UI components. For properties, you define an `@property` in the header. For events, you define a `RCTDirectEventBlock` in the view's `@interface`. + +```objc +// RCTCustomWebView.h +@property (nonatomic, copy) NSString *finalUrl; + +// RCTCustomWebView.m +@interface RCTCustomWebView () + +@property (nonatomic, copy) RCTDirectEventBlock onNavigationCompleted; + +@end +``` + +Then expose it in the view manager's `@implementation`. + +```objc +// RCTCustomWebViewManager.m +RCT_EXPORT_VIEW_PROPERTY(onNavigationCompleted, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(finalUrl, NSString) +``` + +### Extending Existing Events + +You should refer to [RCTWebView.m](https://github.com/facebook/react-native/blob/master/React/Views/RCTWebView.m) in the React Native codebase to see what handlers are available and how they are implemented. You can extend any methods here to provide extra functionality. + +By default, most methods aren't exposed from RCTWebView. If you need to expose them, you need to create an [Objective C category](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html), and then expose all the methods you need to use. + +```objc +// RCTWebView+Custom.h +#import + +@interface RCTWebView (Custom) +- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; +- (NSMutableDictionary *)baseEvent; +@end +``` + +Once these are exposed, you can reference them in your custom web view class. + +```objc +// RCTCustomWebView.m +- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL allowed = [super webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + + if (allowed) { + NSString* url = request.URL.absoluteString; + if (url && [url isEqualToString:_finalUrl]) { + if (_onNavigationCompleted) { + NSMutableDictionary *event = [self baseEvent]; + _onNavigationCompleted(event); + } + } + } + + return allowed; +} +``` + +## JavaScript Interface + +To use your custom web view, you'll need to create a class for it. Your class must: + +* Export all the prop types from `WebView.propTypes` +* Return a `WebView` component with the prop `nativeComponent` set to your native component (see below) + +To get your native component, you must use `requireNativeComponent`: the same as for regular custom components. However, you must pass in an extra third argument, `WebView.extraNativeComponentConfig`. This third argument contains prop types that are only required for native code. + +```js +import { Component } from 'react'; +import { WebView, requireNativeComponent } from 'react-native'; + +export default class CustomWebView extends Component { + static propTypes = WebView.propTypes + + render() { + return ( + + ); + } +} + +const RCTCustomWebView = requireNativeComponent( + 'RCTCustomWebView', + CustomWebView, + WebView.extraNativeComponentConfig +); +``` + +If you want to add custom props to your native component, you can use `nativeComponentProps` on the web view. + +For events, the event handler must always be set to a function. This means it isn't safe to use the event handler directly from `this.props`, as the user might not have provided one. The standard approach is to create a event handler in your class, and then invoking the event handler given in `this.props` if it exists. + +If you are unsure how something should be implemented from the JS side, look at [WebView.ios.js](https://github.com/facebook/react-native/blob/master/Libraries/Components/WebView/WebView.ios.js) in the React Native source. + +```js +export default class CustomWebView extends Component { + static propTypes = { + ...WebView.propTypes, + finalUrl: PropTypes.string, + onNavigationCompleted: PropTypes.func, + }; + + static defaultProps = { + finalUrl: 'about:blank', + }; + + _onNavigationCompleted = (event) => { + const { onNavigationCompleted } = this.props; + onNavigationCompleted && onNavigationCompleted(event); + } + + render() { + return ( + + ); + } +} +``` + +Just like for regular native components, you must provide all your prop types in the component to have them forwarded on to the native component. However, if you have some prop types that are only used internally in component, you can add them to the `nativeOnly` property of the third argument previously mentioned. For event handlers, you have to use the value `true` instead of a regular prop type. + +For example, if you wanted to add an internal event handler called `onScrollToBottom`, you would use, + +```js +const RCTCustomWebView = requireNativeComponent( + 'RCTCustomWebView', + CustomWebView, + { + ...WebView.extraNativeComponentConfig, + nativeOnly: { + ...WebView.extraNativeComponentConfig.nativeOnly, + onScrollToBottom: true, + }, + } +); +``` diff --git a/docs/HeadlessJSAndroid.md b/docs/HeadlessJSAndroid.md index 005e43376b7937..683ae233793414 100644 --- a/docs/HeadlessJSAndroid.md +++ b/docs/HeadlessJSAndroid.md @@ -6,7 +6,7 @@ category: Guides (Android) permalink: docs/headless-js-android.html banner: ejected next: signed-apk-android -previous: native-components-android +previous: custom-webview-android --- Headless JS is a way to run tasks in JavaScript while your app is in the background. It can be used, for example, to sync fresh data, handle push notifications, or play music. diff --git a/docs/LinkingLibraries.md b/docs/LinkingLibraries.md index 315a0f038365de..cef7f99451e961 100644 --- a/docs/LinkingLibraries.md +++ b/docs/LinkingLibraries.md @@ -6,7 +6,7 @@ category: Guides (iOS) permalink: docs/linking-libraries-ios.html banner: ejected next: running-on-simulator-ios -previous: native-components-ios +previous: custom-webview-ios --- Not every app uses all the native capabilities, and including the code to support diff --git a/docs/NativeComponentsAndroid.md b/docs/NativeComponentsAndroid.md index c32622d44ffe19..adb50eea9573a9 100644 --- a/docs/NativeComponentsAndroid.md +++ b/docs/NativeComponentsAndroid.md @@ -5,7 +5,7 @@ layout: docs category: Guides (Android) permalink: docs/native-components-android.html banner: ejected -next: headless-js-android +next: custom-webview-android previous: native-modules-android --- diff --git a/docs/NativeComponentsIOS.md b/docs/NativeComponentsIOS.md index 5f1f02d71d0ec3..6bf345180fa3fd 100644 --- a/docs/NativeComponentsIOS.md +++ b/docs/NativeComponentsIOS.md @@ -5,7 +5,7 @@ layout: docs category: Guides (iOS) permalink: docs/native-components-ios.html banner: ejected -next: linking-libraries-ios +next: custom-webview-ios previous: native-modules-ios ---