Android Question Wait a Javascript value

max123

Well-Known Member
Licensed User
Longtime User
Hi all,

I 've ported this @stevel05 B4J CodeMirror wrapper to B4A
https://www.b4x.com/android/forum/threads/codemirror-wrapper-and-example.125775/#post-785682

Now I need to get values back from Javascript but I've some problems.

Steve in his B4J code implemented itself a Javascript interface on a JavaFX WebView, ExecuteJavaScript command can execute JS and return back an object so it can back with just one call.

B4X:
'Executes a script in the context of the current page.
Public Sub ExecuteScript(Script As String) As Object
    Return TJO.RunMethod("executeScript",Array As Object(Script))
End Sub

B4J CodeMirrorWrapper to get editor code do this:
B4X:
'Get the current code editor text code
Public Sub GetCode() As String
    Return ExecuteScript("editor.getValue();")  ' A single call to inject JS and get back return value to B4J calling module that request it (Main or class module)
End Sub

In my code I use WebViewExtras and ExecuteJavascript method do not return values, so I just created a JS function to get a value and then JS call a B4A sub to pass back it.

The problem here is that this require some time and I do not know how to wait a value.
It always return non last value assigned to a global variable (from previous call).

I've even tried to reset it and wait with while loop (not good option, just for test) while the value change, but as expected it enter in an infinite loop.

Even I do not like that require a global variable.

Here my attemp:
B4X:
'Get the current text on this CodeEditor.
Public Sub GetCode As String
    '    Code = ""  ' Code is a String global variable
    wve.executeJavascript(mWebView, "getValue();")

'    Return wve.executeJavascript(mWebView, "editor.getValue();"  ' This cannot be used because ExecuteJavascript
                                                                ' method return nothing
    Return Code
End Sub

Public Sub codemirrortext(Value As String)  ' Called by JS to return back the value
    Code = Value
    Log(">>> CodeMirror text from JS: " & CRLF & Code)
End Sub

JS Code:
function GetValue() {
  var text = editor.getValue();
  B4A.CallSub('codemirrortext', true, text);
}

I need to do it on a lots of methods (in the wrapper class).

Wait For I think cannot help here because the sub is called by JS and not B4A, right ?

Please can someone help me to know how to do it in an elegant way ?

Thanks
 
Last edited:

drgottjr

Expert
Licensed User
Longtime User
you say:
"In my code I use WebViewExtras and ExecuteJavascript method do not return values, so I just created a JS function to get a value and then JS call a B4A sub to pass back it."

you can do precisely the same thing using ExecuteJavascript. you can create and run the same function with webviewextras. unfortunately, the result will be the same (in your case, no result)

the crucial element is the js function getValue(). it is obviously based on the value returned by editor.getValue(). if that value is not available (that is, populated), you can wait all day without a result.

what we do not know - although i suspect it is the case - is editor.getValue() asynchronous? that is to say: there is no value until the user does something in the editor. is that so? you don't say, but that is my impression. if it is not asynchronous, then whatever value is available at the time you call getValue() will be the value returned, which could be nothing. you would have to keep calling getValue() over and over until, eventually, a value is available, if ever ...

if editor.getValue() (and, by extension, the js function getValue() that drives it) is asynchronous, you need a callback or a listener (+ a callback) or similar (eg, promise). in other words, a kind of "wait for" on the js side. when the value is available, the B4A.callsub() can be triggered. at that point, you could also use a b4a wait for to handle that event.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Many thanks for reply,

teorically when html page is loaded and editor is initialized in JS code, then the editor exposes all methods without use callbacks, like editor.getValue() should return just a text of editor that is just a styled textarea.

editor.setValue(text) set it.

It do not wait user intervention, just get a text string and return it, if no text just return a void string.

Calling JS function that get control text and return editor.getValue() and wait a B4A callback event require some milliseconds, because the function return a value and in this case I always set the editor text in a callback to a global variable, it always return non last, the previous.

If I set it to void string "" then call a JS function that return the value, it return void string, only on the B4A event the string is populated.

I've others getters to implement like editor.getSelection(), editor.getCursorPosition() etc ...

Stevel wrote a class to do calls to/from B4X/JS using JavaObject to JavaFX WebView, on this I can just do:
B4X:
Dim Text As String = ExecuteJavascript("editor.getText();")

Maybe I need to adapt the Steve JFX WebView wrapper to native B4A WebView to get a B4A command that execute function and return back value in the same call ?
 
Last edited:
Upvote 0

drgottjr

Expert
Licensed User
Longtime User
B4X:
    wve.executeJavascript(mWebView, "getValue();")
    Return Code

this is wrong. you are returning the code variable before the executejavascript call has finished.


after this:
wve.executeJavascript(mWebView, "getValue();")
try this:
wait for codemirrortext(Value As String)
Code = Value


you can then delete the codemirrortext sub (wait for handles it. it doesn't have to be an actual sub).

see what you get.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Thanks, yes I know that is wrong and return a value before JS return it, but I'm here to search a solution

So you mean this ?
B4X:
'Get the current text on this CodeEditor.
Public Sub GetCode As String   '  <<<< BECAUSE WE USED WAIT FOR IT SHOULD RETURN RESUMABLE SUB
    Code = ""  ' Just to test we reset it. Code is a String global variable, but not need it at all, just used here to assign the value. This need to be removed, just return a value from sub
    wve.ExecuteJavascript(mWebView, "getValue();")
                                                     
    Wait For codemirrortext(Value As String)
    Code = Value
                                                     
    Return Code
End Sub

Public Sub codemirrortext(Value As String)
    Log(">>> CodeMirror text from JS: " & CRLF & Value)
End Sub

tried it but is not possible
B4X:
Public Sub GetCode As String
should return ResumableSub, cannot return a string

Yesterday I've tried even with Complete, something like this, but I had the same problem
B4X:
Wait For(codemirrortext) Complete (Value As String)
 
Last edited:
Upvote 0

DonManfred

Expert
Licensed User
Longtime User
Inform about Resumeable subs and how to use a specific return type.
You are not using it right.

 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Thanks @DonManfred ,

I've read it a lots of times but I've difficult understand.

So if:
B4X:
Wait For(Sum(1, 2)) Complete (Result As Int)
works the:
B4X:
Wait For (codemirrortext(atext)) Complete (o As Object) 'Return null from sub just to do complete event
or:
B4X:
Wait For (codemirrortext(atext)) Complete (Text As String) 'Return a text value
should work too ?

And probably I need to put a Sleep(0) in the codemirrortext sub ?

The problem here is that I cannot pass the text as argument, this is returned from JS.

Please point me to the right direction with a simple code, tried different ways but nothing worked.

Thanks all
 
Last edited:
Upvote 0

teddybear

Well-Known Member
Licensed User
Public Sub GetCode As String
should return ResumableSub, cannot return a string

Yesterday I've tried even with Complete this way but I had the same problem
ResumableSub can return string as DonManfred said that

B4X:
Public Sub GetCode As  ResumableSub
    Code = ""  ' Just to test we reset it. Code is a String global variable, but not need it at all, just used here to assign the value. This need to be removed, just return a value from sub
    wwe.ExecuteJavascript(mWebView, "getValue();")
                                                        
    Wait For codemirrortext(Value As String)
    Code = Value                                                        
    Return Code
End Sub
You need to do
B4X:
 Wait For(GetCode) Complete (Result AsString)
log(Result)
to get result.
 
Upvote 0

DonManfred

Expert
Licensed User
Longtime User
Please point me to the right direction with a simple code
I already DID. It is the link above about resumeable subs.
The link does not help you learning javascript though.

I can not help with such javascript issues as i do not have much experience with javascript.

Basically executejavascript executes something. if you just place a getvalue(); here i expect there is a javascriptmethod named getValue() already in the javascript. I can´t answer what this sub is doing. I did not wrote it nor you posted it.

Upload a small project showing the problem. Note that you should post a project with WORKING javascript.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
I already DID. It is the link above about resumeable subs.
The link does not help you learning javascript though.

I can not help with such javascript issues as i do not have much experience with javascript.

Basically executejavascript executes something. if you just place a getvalue(); here i expect there is a javascriptmethod named getValue() already in the javascript. I can´t answer what this sub is doing. I did not wrote it nor you posted it.

Upload a small project showing the problem. Note that you should post a project with WORKING javascript.
Yes, I posted it in the first post:
JavaScript:
function GetValue() {
  var text = editor.getValue();
  B4A.CallSub('codemirrortext', true, text);
}

Substantially it just return the text editor text after initialized it.

Posting a simple project it not possible, just to import a codemirror library the code is a bit complex and involve a CodeMirrorWrapper class that use other classes, the full minimal project has 7-8 classes.

But for a case you can just adopt the normal JS call, just call a function that eg, just return back a string like
JavaScript:
B4A.CallSub("GetMyString", true, "Hello");
by calling B4A.CallSub(.....)

Just I need to get it in the public sub that call ExecuteJavascript and not in the callback sub.... with a sort of Wait For.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Hi @teddybear, thanks for reply,

but in the way you show, this go outside a CodeMirrorWrapper class, so in the main:
B4X:
Wait For(GetCode) Complete (Result As String)
log(Result)

My purpose is to use original code like this, as I explained the getValue() is only one of others in the class:

B4X:
Dim code As String = CodeMirrorWrapper1.GetCode
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
@DonManfred as you request I tried to reproduce it in a small project, just returning a string, no codemirror.

The codemirror code just do the same, get a string and return it. But I need it in the sub called from main, the same that ExecuteJavascript, in my case getValue that return JS editor.getValue() to the main this way
B4X:
Dim code As String = CodeMirrorWrapper1.GetCode  ' Get current editor text or void string if nothing
This should be done after the page is loaded and editor initialized on JS side, note the _Ready event in my code, after this editor can be used.

Maybe I can inject the JS function directly witth WebViewExtras without put it in the html.

Attached the zip.

Thanks
 

Attachments

  • GET_JS_VALUE.zip
    7.7 KB · Views: 225
Last edited:
Upvote 0

teddybear

Well-Known Member
Licensed User
Attached the zip.

Thanks
Main

B4X:
Sub obj_Ready()  'HERE HTML IS LOAD ON A WEBVIEW
    Log("Main:_Ready")
    wait For(obj.GetJavascriptString) complete(text As String)
    Log("Main: text: " & text)
End Sub

MyClass
Add a boolean waiting for script completion
B4X:
Sub Class_Globals
    ...
    Private jsRunning As Boolean=True
    Private jsText As String
    ...
End Sub
Public Sub GetJavascriptString () As ResumableSub
    wve.executeJavascript(WebView1, "getString();")
    Do While jsRunning
        Sleep(50)
    Loop
    Return jsText
End Sub

Public Sub JavascriptCallback(Text As String)
    Log("JS String: " & Text)
    jsText = Text
    jsRunning = False
End Sub
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Many thanks @teddybear,

Now that I read this I realized that there is no way on Android to do what Steve do on a B4J JavaFX WebView, Android only can do it asynch and require callback to no block current thread.
There isn't a way to evaluate Javascript synchronously on Android (i.e. on the current thread) so your best bet is to use evaluateJavascript then wait for the callback:
public void getDataFromJs(String command, WebView webView) {
webView.evaluateJavascript("(function() { return " + command + "; })();", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
returnDataFromJs(s);
}
});
}

public returnDataFromJs(String data) {
// Do something with the result.
}

There isn't a method to evaluate Javascript on the current thread, as Javascript operations can take a while (the JS engine needs time to spin up) and this can block the current thread.

I appreciate a lots your help, but as I explained I want avoid to assign any value to a global variable, I've used it just for tests, my goal at this point is to use Wait For in a way to wait a value directly on the function that evalutate and return a value to the main.

From your code, suppose I have 50 getters in my CodeMirrorWrapper class, to do this I suppose I need to create 100 global variables, each, one to handle value and one to handle jsRunning (one for each to not conflict with others).

Another problem is that this should be done directly on the class and access to the main this way:
B4X:
Dim Code As String = CodeEditor1.GetCode  ' Get current editor text or void string if nothing
Dim Pos As Int = CodeEditor1.getCursorPosition
Dim Selection As String = CodeEditor1.getSelection

I want avoid to use Wait For in the main to retrieve editor values and properties, suppose you want to get a cursor position or somethong else....
B4X:
Wait For(CodeEditor1.getCursorPosition) Complete(Pos As Int)
Log("Cursor position: " & Pos)

Wait For(CodeEditor1.getSelection) Complete(Text As String)
Log("Selected text: " & Text)

For me this is not a problem on develop, but because this is a class (a library) I want avoid the user use Wait For for any library getters.

Do you have idea on a way that I can do it directly inside a class?
Another option can be use callbacks to the main, using CallSub, CallSubDelayed, but this is to avoid, suppose to get a value, here require the main have a callback for any getter, 50 getters..... ooooh and a lots of global variables to handle values....

Anyway now I will try your advices, but I think I cannot use it in a final class.

Many thanks
 
Last edited:
Upvote 0

teddybear

Well-Known Member
Licensed User
I want avoid to use Wait For in the main to retrieve editor values and properties, suppose you want to get a cursor position or somethong else....

For me this is not a problem on develop, but because this is a class (a library) I want avoid the user use Wait For for any library getters.
It is not a problem to avoid using Wait For in the main. you can put waitfor into MyClass.

B4X:
Sub WebView1_PageFinished (Url As String)
    wait For(GetJavascriptString) complete(result As Boolean)   
    If SubExists(mCallBack, mEvent & "_Ready") Then CallSubDelayed(mCallBack, mEvent & "_Ready")
End Sub

public Sub GetJsS As String   
    Return jsText
End Sub

Public Sub GetJavascriptString () As ResumableSub
    wve.executeJavascript(WebView1, "getString();")
    Do While jsRunning
        Sleep(50)
    Loop
    Return True
End Sub

Main
B4X:
Sub obj_Ready()  'HERE HTML IS LOAD ON A WEBVIEW
    Log("Main:_Ready")
    Dim text As String = obj.GetJsS
    Log("Main: text: " & text)
End Sub
 
Upvote 0

teddybear

Well-Known Member
Licensed User
suppose I have 50 getters in my CodeMirrorWrapper class, to do this I suppose I need to create 100 global variables, each, one to handle value and one to handle jsRunning (one for each to not conflict with others).
I don't know how you wrap your class, if you have 50 getters, it is necessary to create 50 variables.they are not global, and they are just members of the class.
User can create an instance of the class by Initialize and get values and properties by getters of the instance
B4X:
Wait For(CodeEditor1.getCursorPosition) Complete(Pos As Int)
Log("Cursor position: " & Pos)

Wait For(CodeEditor1.getSelection) Complete(Text As String)
Log("Selected text: " & Text)
Ditto, In class CodeEditor, you can add members CursorPosition and Selection, I think one jsRunning is enough.
 
Upvote 0

stevel05

Expert
Licensed User
Longtime User
It's a long time since I've looked at this on Android so I might be wrong, but you will only ever have one callback at a time, you could try creating a callback manager method that delegates to the required sub based on a parameter.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Many thanks @stevel05 , is much simpler on JavaFX WebView, just execute and return back in one call.

And how I can create a Callback Manager? I've no idea?
 
Upvote 0

stevel05

Expert
Licensed User
Longtime User
As I say it's a long time since I looked at it so I don't know if it's possible to send two parameters from Javascript (Or an array), but it would look something like this:

B4X:
Public Sub JavascriptCallback(ID as String, Text As String)
    Select Id
        Case "First"
            First(Text)
        Case "Second"
            Second(Text)
    End Select
End Sub

Sub First(Text As String)
    'Do whatever'
End Sub

Sub Second(Text As String)
    'Do Whatever'
End Sub
 
Upvote 0

stevel05

Expert
Licensed User
Longtime User
Just had a quick look, you may need to send the parameters from Javascript (as many as you need) as a json string and decode it in the callback manager.
 
Upvote 0
Top