How to ask for a value from my add-on's python code from JS?

How can JS I inject into the reviewer webview request values back from my add-on’s Python side?

An untested idea:

Your add-on could insert some values embedded in HTML into the card, than the JS code of reviewer could fetch these values via document.getElementByXX().

The webview_did_receive_js_message hook is the way to go.

Here is a not-so-minimal example of how I use it in one of my add-ons:

5 Likes

pycmd doesn’t seem ideal to me, because the only way the calling JS can get values back from the Python is by having the Python update the HTML, then the JS has to read the HTML. So weird and round-a-bout.

pycmd(`myaddon:${someargument}`);
const result = Number(document.querySelector('#predetermined-unlikely-to-be-used-by-the-user-selector').dataset.result);
handle_calls(handled: tuple[bool, Any], message: str, context: Any) -> tuple[bool, Any]:
    namespace = 'myaddon'
    parts = message.split(':')
    if parts[0] != namespace:
        return handled
    number = int(parts[1])
    square = number ** number
    web = get_webview_for_context(context)
    web.eval(f'''
const input = document.querySelector('#predetermined-unlikely-to-be-used-by-the-user-selector') || document.createElement('input');
input.setAttribute('hidden', '');
input.setAttribute('data-result', '{square}');
input.setAttribute('id', 'predetermined-unlikely-to-be-used-by-the-user-selector');
try {
    document.body.removeChild(input);
} catch (e) {}
document.body.appendChild(input);
''')
    return (True, None)

This seems p ugly so I’m hesitant to use pycmd for now, suspecting there’s got to be a better way.

I guess the JS could also provide some callback:

pycmd(`myaddon:square(${number}, function (result) { ... })`);
// or 
const handleResult = function (result) {
    ...
};
pycmd(`myaddon:square:${number}:handleResult`);

But I don’t know how to evaluate the callbacks as closures in their original contexts

Who says that? You can run JS commands from Python with AnkiWebView.eval, e.g.:

Python

import json

args = {
  "foo": 1,
  "bar": True,
}

editor.web.eval(f"MyLibrary.doThatThing({json.dumps(args)}); ")

JS

const MyLibrary = {
  doThatThing: (foo, bar) => {
    console.log(`foo: ${foo}, bar: ${bar}`);
    // foo: 1, bar: true
  },
}

And if you want to use the return value of a JS function in Python, there’s AnkiWebView.evalWithCallback.

5 Likes

So the JS has to bind something to the global namespace that the Python can call with the return value?

def handle_pycmd(handled: tuple[bool, Any], message: str, context: Any) -> tuple[bool, Any]:
    if not message.startswith('myaddon:'):
        return handled
    ret = calculate(message)
    get_webview_for_context(context).eval(f'''MyAddon.acceptValueBack({ret})''')
    return (True, None)
window.MyAddon = {
  acceptValueBack: (ret) => {
    // ...
  }
}

It’s similar to the callback version. The JS that called pycmd could even set window.MyAddon.acceptValueBack itself ofc (i guess).

Is this the best solution we have available in Anki?

The pycmd bridge is bidirectional. You can pass a callback function to pycmd…:

pycmd("my-addon", console.log)

…which will then receive the JSON-serialized return value of your message handler:

def on_webview_did_receive_js_message(
    handled: Tuple[bool, Any], message: str, context: Any
):
    if message != "my-addon":
        return handled

    return_value = ...

    return (True, return_value)

Note that this approach has the disadvantage of being synchronous on the Python side.

Using webview.eval() on the way back offers you the freedom to run more expensive queries in the background and pass it to a JS handler when the data is ready.

7 Likes