Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WebView] Add props for overriding native component #10946

Closed
wants to merge 13 commits into from
Closed
31 changes: 25 additions & 6 deletions Libraries/Components/WebView/WebView.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -188,6 +196,18 @@ class WebView extends React.Component {
* @platform android
*/
saveFormDataDisabled: 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 = {
Expand Down Expand Up @@ -243,8 +263,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 =
<RCTWebView
<NativeWebView
{...this.props.nativeComponentProps}
ref={RCT_WEBVIEW_REF}
key="webViewKey"
style={webViewStyles}
Expand Down Expand Up @@ -380,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: {
Expand Down
38 changes: 28 additions & 10 deletions Libraries/Components/WebView/WebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -357,6 +368,18 @@ class WebView extends React.Component {
'always',
'compatibility'
]),

/**
* 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
};

state = {
Expand Down Expand Up @@ -417,8 +440,11 @@ class WebView extends React.Component {

const messagingEnabled = typeof this.props.onMessage === 'function';

let NativeWebView = this.props.nativeComponent || RCTWebView;

var webView =
<RCTWebView
<NativeWebView
{...this.props.nativeComponentProps}
ref={RCT_WEBVIEW_REF}
key="webViewKey"
style={webViewStyles}
Expand Down Expand Up @@ -578,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: {
Expand Down
74 changes: 40 additions & 34 deletions React/Views/RCTComponentData.m
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ - (void)setProps:(NSDictionary<NSString *, id> *)props forShadowView:(RCTShadowV

- (NSDictionary<NSString *, id> *)viewConfig
{
NSMutableDictionary *propTypes = [NSMutableDictionary new];
NSMutableArray<NSString *> *bubblingEvents = [NSMutableArray new];
NSMutableArray<NSString *> *directEvents = [NSMutableArray new];

Expand All @@ -395,42 +396,47 @@ - (void)setProps:(NSDictionary<NSString *, id> *)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<NSString *> *(*)(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<NSString *> *(*)(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]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {

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;
Expand All @@ -101,14 +101,14 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {

// 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) {
Expand Down Expand Up @@ -184,15 +184,15 @@ 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(
webView.getId(),
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
Expand All @@ -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) {
Expand Down Expand Up @@ -257,14 +257,18 @@ 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;
}

messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME);
addJavascriptInterface(createReactWebViewBridge(this), BRIDGE_NAME);
linkBridge();
} else {
removeJavascriptInterface(BRIDGE_NAME);
Expand Down Expand Up @@ -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();
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down