Skip to content

Commit 350177d

Browse files
onMessage and injectJavaScript
1 parent c7ef092 commit 350177d

File tree

10 files changed

+137
-19
lines changed

10 files changed

+137
-19
lines changed

.gitattributes

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55
babel.config.js linguist-vendored=true
66
metro.config.js linguist-vendored=true
77
example/transformer.js linguist-vendored=true
8-
example/android linguist-vendored=true
9-
example/ios linguist-vendored=true
8+
example/android/* linguist-vendored=true
9+
example/ios/* linguist-vendored=true
10+
Podfile linguist-vendored=true
11+
*.podspec linguist-vendored=true

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ Called when page finishes loading.
5656
|----|--------|
5757
|function|No|
5858

59+
### `onMessage`
60+
61+
Called when a message is sent from the webview.
62+
63+
#### iOS
64+
65+
Use `webkit.messageHandlers.jsMessageHandler.postMessage(message)` to send your message. Supported types are object, string, number, and boolean. You will receive `message` as a property of `nativeEvent`.
66+
67+
#### Android
68+
69+
Use `JSBridge.postString(string)` to send a string. Use `JSBridge.postNumber(number)` to send a number. Use `JSBridge.postBoolean(boolean)` to send a boolean. Use `JSBridge.postNull()` to send `null`. You will receive your message as a property of `nativeEvent`.
70+
71+
|Type|Required|
72+
|----|--------|
73+
|function|No|
74+
5975
### `keyboardDisplayRequiresUserAction`
6076

6177
When set `false` allows [`HTMLElement.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus), and [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autofocus) to display the keyboard.
@@ -77,11 +93,15 @@ When set to `true` this will hide the default keyboard accessory view.
7793
### `focus()`
7894

7995
#### Android
80-
Calls [`requestFocus`](https://developer.android.com/reference/android/webkit/WebView#requestFocus(int,%20android.graphics.Rect)).
96+
Calls [`requestFocus`](https://developer.android.com/reference/android/webkit/WebView#requestFocus(int,%20android.graphics.Rect)) and shows the keyboard.
8197

8298
#### iOS
8399
Calls [`HTMLElement.blur()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/blur) and [`HTMLElement.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus) on [`document.activeElement`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/activeElement). It won't work if [`keyboardDisplayRequiresUserAction`](#keyboardDisplayRequiresUserAction) is `true` or if `document.activeElement` is not [`document.activeElement`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/activeElement). It is recommended to just focus your field from JavaScript instead of calling this method, calling blur beforehand may be required.
84100

101+
### `injectJavaScript(string)`
102+
103+
Executes the JavaScript string.
104+
85105
## Requirements
86106

87107
- React Native 0.60 or later

android/src/main/java/com/reactnativewebviewalternative/WebViewAlternativeManager.kt

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
11
package com.reactnativewebviewalternative
22

33
import android.annotation.SuppressLint
4-
import android.content.Context
5-
import android.util.Log
4+
import android.os.Build
65
import android.view.MotionEvent
76
import android.view.inputmethod.InputMethodManager
7+
import android.webkit.JavascriptInterface
88
import android.webkit.WebView
99
import android.webkit.WebViewClient
1010
import androidx.core.content.ContextCompat.getSystemService
11-
import com.facebook.react.bridge.ReactApplicationContext
12-
import com.facebook.react.bridge.ReactContext
13-
import com.facebook.react.bridge.ReadableArray
11+
import com.facebook.react.bridge.*
1412
import com.facebook.react.common.MapBuilder
1513
import com.facebook.react.uimanager.SimpleViewManager
1614
import com.facebook.react.uimanager.ThemedReactContext
1715
import com.facebook.react.uimanager.annotations.ReactProp
1816
import com.facebook.react.uimanager.events.RCTEventEmitter
1917

2018

21-
class WebViewAlternativeManager(reactContext: ReactApplicationContext): SimpleViewManager<WebView>() {
22-
val reactContext = reactContext
23-
19+
class WebViewAlternativeManager(private val reactContext: ReactApplicationContext): SimpleViewManager<WebView>() {
2420
enum class Command(val commandId: Int) {
2521
LOAD_HTML_STRING(1),
2622
FOCUS(2),
23+
INJECT_JAVASCRIPT(3),
2724
}
2825

2926
companion object {
@@ -47,6 +44,44 @@ class WebViewAlternativeManager(reactContext: ReactApplicationContext): SimpleVi
4744
}
4845
}
4946

47+
webView.settings.javaScriptEnabled = true
48+
webView.addJavascriptInterface(object : Object() {
49+
fun sendMessage(message: WritableMap) {
50+
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(
51+
webView.id,
52+
"message",
53+
message)
54+
}
55+
56+
@JavascriptInterface
57+
fun postNull() {
58+
val writableMap = Arguments.createMap()
59+
writableMap.putNull("message")
60+
sendMessage(writableMap)
61+
}
62+
63+
@JavascriptInterface
64+
fun postString(message: String) {
65+
val writableMap = Arguments.createMap()
66+
writableMap.putString("message", message)
67+
sendMessage(writableMap)
68+
}
69+
70+
@JavascriptInterface
71+
fun postBoolean(message: Boolean) {
72+
val writableMap = Arguments.createMap()
73+
writableMap.putBoolean("message", message)
74+
sendMessage(writableMap)
75+
}
76+
77+
@JavascriptInterface
78+
fun postNumber(message: Double) {
79+
val writableMap = Arguments.createMap()
80+
writableMap.putDouble("message", message)
81+
sendMessage(writableMap)
82+
}
83+
}, "JSBridge")
84+
5085
return webView
5186
}
5287

@@ -77,15 +112,22 @@ class WebViewAlternativeManager(reactContext: ReactApplicationContext): SimpleVi
77112
imm != null && imm.showSoftInput(root, InputMethodManager.SHOW_IMPLICIT)
78113
}
79114
}
115+
Command.INJECT_JAVASCRIPT.commandId -> {
116+
if (args != null) {
117+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
118+
root.evaluateJavascript(args.getString(0), null)
119+
}
120+
}
121+
}
80122
}
81123
}
82124

83125
override fun getCommandsMap(): Map<String, Int> {
84-
return MapBuilder.of("loadHTMLString", Command.LOAD_HTML_STRING.commandId, "focus", Command.FOCUS.commandId)
126+
return MapBuilder.of("loadHTMLString", Command.LOAD_HTML_STRING.commandId, "focus", Command.FOCUS.commandId, "injectJavaScript", Command.INJECT_JAVASCRIPT.commandId)
85127
}
86128

87129
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Map<String, String>> {
88-
return MapBuilder.of("load", MapBuilder.of("registrationName", "onLoad"))
130+
return MapBuilder.of("load", MapBuilder.of("registrationName", "onLoad"), "message", MapBuilder.of("registrationName", "onMessage"))
89131
}
90132

91133
// override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Map<String, Map<String, String>>> {

example/android/app/src/main/java/com/WebviewAlternativeExample/MainApplication.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import android.app.Application;
44
import android.content.Context;
5+
import android.os.Build;
6+
import android.webkit.WebView;
7+
58
import com.facebook.react.PackageList;
69
import com.facebook.react.ReactApplication;
710
import com.facebook.react.ReactNativeHost;
@@ -49,6 +52,9 @@ public void onCreate() {
4952
super.onCreate();
5053
SoLoader.init(this, /* native exopackage */ false);
5154
initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); // Remove this line if you don't want Flipper enabled
55+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
56+
WebView.setWebContentsDebuggingEnabled(true);
57+
}
5258
}
5359

5460
/**

example/src/App.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
1-
import React, { useRef } from 'react'
2-
import { StyleSheet, View, Platform } from 'react-native'
1+
import React, { useRef, useState } from 'react'
2+
import { StyleSheet, View, Platform, Button, Text } from 'react-native'
33
import WebView, { WebViewRef } from 'react-native-webview-alternative'
44

55
const demo1: string = require('./demo1.html')
66

77
export default function App() {
8+
const [message, setMessage] = useState('')
89
const ref = useRef<WebViewRef>(null)
910

1011
return (
1112
<View style={styles.container}>
13+
<Button
14+
title="Set input text to 'Hello World'"
15+
onPress={() => {
16+
ref.current?.injectJavaScript('input.value = "Hello World"')
17+
}}
18+
/>
19+
<Text>{message}</Text>
1220
<WebView
1321
ref={ref}
1422
source={{ html: demo1 }}
1523
keyboardDisplayRequiresUserAction={false}
1624
style={styles.webView}
1725
scrollEnabled={false}
1826
hideKeyboardAccessoryView
27+
onMessage={({ nativeEvent: { message } }) => (
28+
setMessage(String(message)), console.log(message, typeof message)
29+
)}
1930
onLoad={() => Platform.OS === 'android' && ref.current?.focus()}
2031
/>
2132
</View>

example/src/demo1.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
<h6>Helloo</h6>
1818
<input type="text" id="asd" autofocus />
1919
<script type="text/javascript">
20-
setTimeout(() => {
21-
// document.getElementById('asd').focus();
22-
}, 1000);
20+
window.input = document.getElementById('asd');
21+
input.addEventListener('input', () => {
22+
console.log(input.value);
23+
typeof webkit === "object" ? webkit.messageHandlers.jsMessageHandler.postMessage(input.value) : JSBridge.postString(input.value)
24+
})
2325
</script>
2426
</body>
2527
</html>

ios/RCTWebViewAlternative.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#import <WebKit/WebKit.h>
22
#import <React/RCTViewManager.h>
33

4-
@interface RCTWebViewAlternative : WKWebView {
4+
@interface RCTWebViewAlternative : WKWebView <WKScriptMessageHandler> {
55
NSNumber * _Nullable _hideKeyboardAccessoryView;
66
NSNumber * _Nullable _keyboardDisplayRequiresUserAction;
77
}
88

99
@property (nonatomic, copy) RCTDirectEventBlock _Nullable onLoad;
10+
@property (nonatomic, copy) RCTDirectEventBlock _Nullable onMessage;
1011

1112
- (BOOL)scrollEnabled;
1213
- (void)setScrollEnabled:(BOOL)enabled;

ios/RCTWebViewAlternative.m

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,15 @@ - (void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView {
132132
method_setImplementation(method, override);
133133
}
134134

135+
#pragma mark - WKScriptMessageHandler
136+
137+
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
138+
if (!self.onMessage) return;
139+
if (message.body == nil) return;
140+
141+
self.onMessage(@{
142+
@"message": message.body,
143+
});
144+
}
145+
135146
@end

ios/WebviewAlternative.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ @implementation WebViewAlternativeManager
1515
- (RCTWebViewAlternative *)view {
1616
RCTWebViewAlternative *view = [RCTWebViewAlternative new];
1717
view.navigationDelegate = self;
18+
[view.configuration.userContentController addScriptMessageHandler:view name:@"jsMessageHandler"];
1819
return view;
1920
}
2021

@@ -42,11 +43,24 @@ - (RCTWebViewAlternative *)view {
4243
}];
4344
}
4445

46+
RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *) reactTag string:(nonnull NSString *)string) {
47+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
48+
UIView *view = viewRegistry[reactTag];
49+
if (!view || ![view isKindOfClass:RCTWebViewAlternative.class]) {
50+
RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
51+
return;
52+
}
53+
54+
[(RCTWebViewAlternative *)view evaluateJavaScript:string completionHandler:nil];
55+
}];
56+
}
57+
4558
RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
4659
RCT_EXPORT_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL)
4760
RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
4861

4962
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
63+
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
5064

5165
#pragma mark - WKNavigationDelegate
5266

src/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ interface WebViewAlternativeProps extends ViewProps, WebViewAlternativePropsIOS
1111
children?: ReactNode
1212
scrollEnabled?: boolean
1313
onLoad?(): void
14+
onMessage?(event: { nativeEvent: { message: object | string | number | boolean } }): void
1415
}
1516

1617
export interface WebViewAlternativeRef {
1718
focus(): void
19+
injectJavaScript(string: string): void
1820
}
1921

2022
export type WebViewRef = WebViewAlternativeRef
@@ -52,6 +54,13 @@ function WebViewAlternative({ source = null, ...props }: WebViewAlternativeProps
5254
undefined,
5355
)
5456
},
57+
injectJavaScript(string: string) {
58+
UIManager.dispatchViewManagerCommand(
59+
findNodeHandle(nativeComponentRef.current),
60+
UIManager.getViewManagerConfig('WebViewAlternative').Commands.injectJavaScript,
61+
[string],
62+
)
63+
},
5564
}))
5665
return <NativeWebViewAlternative {...props} ref={nativeComponentRef} />
5766
}

0 commit comments

Comments
 (0)