Register TextInput change by JS keydown event listener

For a rather elaborate web app I’m working on, it would be really convenient if users could change the value of a TextInput field by pressing a key on the keyboard. I can get the value of TextInput to change, but then this is not registered and passed on. If I manually change the value it does pass on, so I suppose I’m missing some trigger when I use the JS event listener but I can’t figure it out.

Can this be done? Or are there perhaps less elaborate ways to pass values from the keyboard on as well? I’ve sunk so much time in this, please help :frowning:

Minimal example (Bokeh 1.4.0, python 3.7):

main.py

from bokeh.layouts import column
from bokeh.models.widgets import Div, TextInput
from bokeh.plotting import curdoc


def change_div(attr, old, new):
    if old == new: return
    div.text = new

div = Div(text="0")
ti_obj = TextInput(value='0', name='text_input1')


ti_obj.on_change('value_input', change_div)
curdoc().add_root(column(ti_obj, div))

and index.html

{% block css_resources %}
	{{ bokeh_css }}
{% endblock %}

{%block js_resources %}
{{ bokeh_js }}
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-tables-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-gl-1.4.0.min.js"></script>
{% endblock %}

{%block body %}
<body>
    {% block inner_body %}
    {% block contents %}
        {% for doc in docs %}
        {{ embed(doc) if doc.elementid }}
        {% for root in doc.roots %}
            {{ embed(root) | indent(10) }}
        {% endfor %}
        {% endfor %}
    {% endblock %}
    {{ plot_script | indent(8) }}
    <script>
        var tst = '';
        document.addEventListener('keydown', (e) => {
            console.log('key pressed: ' + e.keyCode);
            if (isFinite(e.key)) {document.getElementsByName("text_input1")[0].value = e.key;}
        });
    </script>
    {% endblock %}
</body>
{% endblock %}

So with file structure:

  • my_app
    • main.py
    • templates
      • index.html

I would run with

bokeh serve --show my_app

EDIT:
So I think the issue is that I change the HTML attribute but not the value attribute of the bokehJS object, as explained here: https://stackoverflow.com/questions/45743383/how-can-i-set-the-value-of-an-input-text-field-in-bokeh-from-javascript

I tried to find the corresponding field in the bokeh object by manually inspecting the tree of window.Bokeh.index, but I can’t find the proper field in the bokeh object…

EDIT 2:
Solved it! Took some digging, but the correct property was in window.Bokeh.documents[0]._all_models_by_name._dict[‘text_input1’].value_input. So this works with small modification to index.html:

{% extends base %}
{% block css_resources %}
	{{ bokeh_css }}
{% endblock %}

{%block js_resources %}
{{ bokeh_js }}
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-tables-1.4.0.min.js"></script>
	<script src="https://cdn.pydata.org/bokeh/release/bokeh-gl-1.4.0.min.js"></script>
{% endblock %}

{%block body %}
<body>
    {% block inner_body %}
    {% block contents %}
        {% for doc in docs %}
        {{ embed(doc) if doc.elementid }}
        {% for root in doc.roots %}
            {{ embed(root) | indent(10) }}
        {% endfor %}
        {% endfor %}
    {% endblock %}
    {{ plot_script | indent(8) }}
    <script>
        var tst = '';
        document.addEventListener('keydown', (e) => {
            console.log('key pressed: ' + e.keyCode);
            if (isFinite(e.key)){
                window.Bokeh.documents[0]._all_models_by_name._dict['text_input1'].value_input = e.key;}
        });
    </script>
    {% endblock %}
</body>
{% endblock %}

You can use this to send keystrokes to the server side in general!

2 Likes

Just to add a bit. Instead of using the private _all_models_by_name._dict['text_input1'], you can use get_model_by_name('text_input1').