Skip to content

Commit

Permalink
Add a injectJavaScript method to the WebView component
Browse files Browse the repository at this point in the history
Summary:
Currently, < WebView > allows you to pass JS to execute within the view. This works great, but there currently is not a way to execute JS after the page is loaded. We needed this for our app.

We noticed that the WebView had messaging support added (see #9762) . Initially, this seemed like more than enough functionality for our use case - just write a function that's injected on initial load that accepts a message with JS, and `eval()` it. However, this broke once we realized that Content Security Policy can block the use of eval on pages. The native methods iOS provide to inject JS allow you to inject JS without CSP interfering. So, we just wrapped the native methods on iOS (and later Android) and it worked for our use case. The method injectJavaScript was born.

Now, after I wrote this code, I realized that #8798 exists and hadn't been merged because of a lack of tests. I commend what was done in #8798 as it sorely solves a problem (injecting JS after the initial load) and has more features than what I'
Closes #11358

Differential Revision: D4390425

fbshipit-source-id: 02813127f8cf60fd84229cb26eeea7f8922d03b3
  • Loading branch information
Luke Miles authored and facebook-github-bot committed Jan 7, 2017
1 parent 3d1bdcc commit da9a712
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 2 deletions.
35 changes: 34 additions & 1 deletion Examples/UIExplorer/js/WebViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,35 @@ class MessagingTest extends React.Component {
}
}

class InjectJS extends React.Component {
webview = null;
injectJS = () => {
const script = 'document.write("Injected JS ")'; // eslint-disable-line quotes
if (this.webview) {
this.webview.injectJavaScript(script);
}
}
render() {
return (
<View>
<WebView
ref={webview => { this.webview = webview; }}
style={{
backgroundColor: BGWASH,
height: 300,
}}
source={{uri: 'https://www.facebook.com'}}
scalesPageToFit={true}
/>
<View style={styles.buttons}>
<Button text="Inject JS" enabled onPress={this.injectJS} />
</View>
</View>
);
}
}


var styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -442,5 +471,9 @@ exports.examples = [
{
title: 'Mesaging Test',
render(): ReactElement<any> { return <MessagingTest />; }
}
},
{
title: 'Inject JavaScript',
render(): React.Element<any> { return <InjectJS />; }
},
];
14 changes: 14 additions & 0 deletions Libraries/Components/WebView/WebView.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ class WebView extends React.Component {
);
};

/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.injectJavaScript,
[data]
);
};

/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
Expand Down
14 changes: 14 additions & 0 deletions Libraries/Components/WebView/WebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,20 @@ class WebView extends React.Component {
);
};

/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.injectJavaScript,
[data]
);
};

/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
Expand Down
1 change: 1 addition & 0 deletions React/Views/RCTWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
- (void)reload;
- (void)stopLoading;
- (void)postMessage:(NSString *)message;
- (void)injectJavaScript:(NSString *)script;

@end
5 changes: 5 additions & 0 deletions React/Views/RCTWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ - (void)postMessage:(NSString *)message
[_webView stringByEvaluatingJavaScriptFromString:source];
}

- (void)injectJavaScript:(NSString *)script
{
[_webView stringByEvaluatingJavaScriptFromString:script];
}

- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
Expand Down
12 changes: 12 additions & 0 deletions React/Views/RCTWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ - (UIView *)view
}];
}

RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view injectJavaScript:script];
}
}];
}

#pragma mark - Exported synchronous methods

- (BOOL)webView:(__unused RCTWebView *)webView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
public static final int COMMAND_RELOAD = 3;
public static final int COMMAND_STOP_LOADING = 4;
public static final int COMMAND_POST_MESSAGE = 5;
public static final int COMMAND_INJECT_JAVASCRIPT = 6;

// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
Expand Down Expand Up @@ -477,7 +478,9 @@ protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
"goForward", COMMAND_GO_FORWARD,
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING,
"postMessage", COMMAND_POST_MESSAGE);
"postMessage", COMMAND_POST_MESSAGE,
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT
);
}

@Override
Expand All @@ -504,6 +507,9 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray
throw new RuntimeException(e);
}
break;
case COMMAND_INJECT_JAVASCRIPT:
root.loadUrl("javascript:" + args.getString(0));
break;
}
}

Expand Down

2 comments on commit da9a712

@faceach
Copy link

@faceach faceach commented on da9a712 Mar 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! This is what I need. 👍

@cvillecsteele
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Please sign in to comment.