Change Button Label from Javascript

I am trying to change the Button label text from Javascript. Here is a minimal program that demonstrates what I am trying to do. I did succeed in changing the label text, but I have been told this is not the right way to do it.

This is how I ended up changing the label, which is touching the button object properties directly,

el[0].firstChild.childNodes[0].innerHTML = "new label";

This path changed from a previous version of Bokeh, and may change again, so this is NOT the right way to change the label, hence this post.

Full Code,

#!/usr/bin/python
# -*- coding: utf-8 -*-
import queue
import time
import threading
from flask import Flask
from flask import request, jsonify, Response
from bokeh.layouts import layout, row
from bokeh.models.widgets.buttons import Button
from bokeh.models.callbacks import CustomJS
from bokeh.embed import components
from dominate.tags import *
import dominate
from dominate.util import raw

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

q = queue.Queue()
app = Flask(__name__)

PAGE_URL = "/"
TEST_PORTAL_BUTTON = "/button"
TEST_PORTAL_LISTENER = "/stream"


def dominate_document(title="pywrapBokeh", bokeh_version='1.2.0'):
    d = dominate.document(title=title)
    with d.head:
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))

        script(src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js")

        meta(charset="UTF-8")

    js = """
    var eventSource;

    $(document).ready(function(){{
        var eventSource = new EventSource("{url}");
        eventSource.onmessage = pageEventHandler;
    }});

    function pageEventHandler(e) {{
        //alert(e.data);
        console.log(e.data);

        if (e.data.startsWith("b1")) {{
            el = document.getElementsByClassName("b1_css");
            console.log(el);
            //
            // How to properly set the Button Label???
            el[0].firstChild.childNodes[0].innerHTML = e.data;
        }}

    }};
    """.format(url=TEST_PORTAL_LISTENER)

    with d.body:
        script(raw(js))

    return d


@app.route(PAGE_URL, methods=['GET', 'POST'])
def hello():
    d = dominate_document(title="Test Java Buttons")
    doc_layout = layout()

    b1 =  Button(label="START", width=100, css_classes=["b1_css"])
    on_click = CustomJS(code="""$.getJSON('{}', function(data) {{ }}); """.format(TEST_PORTAL_BUTTON + '?button={}'.format("b1")))
    b1.js_on_click(on_click)

    doc_layout.children.append(row(b1))

    _script, _div = components(doc_layout)
    d.body += raw(_script)
    d.body += raw(_div)
    return "{}".format(d)


@app.route(TEST_PORTAL_BUTTON, methods=['GET', 'POST'])
def url__test_button():
    logger.info("You Pressed: {}".format(request.args))
    d = {"success": True, "msg": None, "from": "url__test_button"}
    return jsonify(d)


@app.route(TEST_PORTAL_LISTENER, methods=['GET'])
def url__test_stream():
    def eventStream():
        while True:
            # wait for source data to be available, then push it
            yield """data: {}\n\n""".format(get_message())
    return Response(eventStream(), mimetype="text/event-stream")


def get_message():
    try:
        item = q.get(block=True)
        logger.info("forwarding {}".format(item["type"]))
        return item["type"]

    except queue.Empty:
        return "None"
    except Exception as e:
        logger.exception("Error processing event: %s" % e)


def send_message():
    count = 0
    while True:
        time.sleep(2)
        count += 1
        d = {"type": "b1 {}".format(count)}
        logger.info("Sending {}".format(d))
        q.put(d)


t = threading.Thread(target=send_message)
t.start()
app.run(host="0.0.0.0", port=6800, debug=False)

As a side note, this code is an example of how to send data between python and the web page with Flask. The python side is sending a new button label every 2s, and when the button is pressed, it calls a python function (via flask url). While I was trying to change the button label, sometimes modifying a button property would break some of these functions, so this is a complicated example to make sure any solution doesn’t break those behaviours.

Is it possible, when making the customJS() for the button, that a function can be added so that the label can be changed?

Without diving in to such a complicated example, I would first say the expected/supported way of changing a Boke Button label from a CustomJS does not involve mucking with the DOM directly:

b = Button(label="foo")
b.js_on_click(CustomJS(args=dict(b=b), code="""
    b.label="bar"
"""))

Is there some specific reason you are doing things differently than this? In general, at this point in time, Bokeh DOM structure is considered an internal implementation detail, and tinkering with it is purely at your own risk.

The button label is being changed from an event initiated on the python side of the code. An event is sent and picked up by a Java EventSource. So its not from a button click I change the label, but from a different async event.

I totally agree I should not be touching the internals of the Bokeh objects.

I am pushing the limits of Bokeh because I am not writing any HTML and very little Java… I think I probably ended up making a poor man’s Bokeh Server, which I have not looked at.

I will look at your CustomJS() example a bit later, because perhaps I can add a Java function in there and then use that to change the label…? Its something I can tinker with.

In that case I would note there is a global Bokeh.index that contains an index of all BokehJS view objects. These all have a .model property that points the actual Bokeh model that backs the view. It’s a little effort to dig through that data structure, but I’d recommend finding the Button model in there somehow, then setting label on it the same as above.

Yes. At one point I wanted to somehow find the ID of the object I was interested in, perhaps a year ago I asked on Gitter. What I ended up with is assigning each object a unique css_classes and then doing a lookup via el = document.getElementsByClassName(name);.

I typed Bokeh.index and got a similar output as you did above. Okay, maybe starting to see it… this object has a _child_views and that seems to have a list of more objects, and each one of those has a .model, and more _child_views.

… found a button, and indeed I can change the .model.label property and the button is updated. This changes EVERYTHING for me.

For looking up objects it seems I can still use the css_classes as a cookie to find the object I want. I can see it in the .model section.

Is Bokeh.index a global variable I can just reference in Java? I will try it out…

Yes, exactly. Would like to have a better/more robust solution for accessing Bokeh objects outside Bokeh CustomJS callbacks some day. Would make a nice project for a new contributor!

Here is an updated version of the demo program that searches the Bokeh.index to find the object (button) and then uses .model.label to change the value.

The key part is the recursive search. I am sure my Java needs work or fails in some corner cases, but two of my demos are working. If I find/make a fix I will post it here. Or if someone suggests improvement…

#!/usr/bin/python
# -*- coding: utf-8 -*-
import queue
import time
import threading
from flask import Flask
from flask import request, jsonify, Response
from bokeh.layouts import layout, row
from bokeh.models.widgets.buttons import Button
from bokeh.models.callbacks import CustomJS
from bokeh.embed import components
from dominate.tags import *
import dominate
from dominate.util import raw

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

q = queue.Queue()
app = Flask(__name__)

PAGE_URL = "/"
TEST_PORTAL_BUTTON = "/button"
TEST_PORTAL_LISTENER = "/stream"


def dominate_document(title="pywrapBokeh", bokeh_version='1.2.0'):
    d = dominate.document(title=title)
    with d.head:
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))
        link(href="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.css".format(
            bokeh_version=bokeh_version),
             rel="stylesheet",
             type="text/css")
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.js".format(
            bokeh_version=bokeh_version))

        script(src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js")

        meta(charset="UTF-8")

    js = """
    var eventSource;

    $(document).ready(function(){{
        var eventSource = new EventSource("{url}");
        eventSource.onmessage = pageEventHandler;
    }});

    function getBokehModelByCSSClass(obj, cssClass) {{
        //console.log(obj);
        if (!("model" in obj)) {{
            return null;
        }}    
        if (!("css_classes" in obj.model)) {{
            return null;
        }}              
        if (obj.model.css_classes.includes(cssClass)) {{
            return obj;
        }}
       
        for (_obj in obj._child_views) {{
            var result = getBokehModelByCSSClass(obj._child_views[_obj], cssClass);
            if (result) {{
                return result;
            }}
        }}
        return null;
    }};

    function first(p) {{
        for (let i in p) return p[i];
    }}

    function pageEventHandler(e) {{
        //alert(e.data);
        console.log(e.data);

        if (e.data.startsWith("b1")) {{
            //console.log("START");
            //console.log(Bokeh.index);
            var result = getBokehModelByCSSClass(first(Bokeh.index), "b1_css");
            console.log(result);
            result.model.label = e.data;
        }}

    }};
    """.format(url=TEST_PORTAL_LISTENER)

    with d.body:
        script(raw(js))

    return d


@app.route(PAGE_URL, methods=['GET', 'POST'])
def hello():
    d = dominate_document(title="Test Java Buttons")
    doc_layout = layout()

    # NOTE: the css_classes entry is used to find this object in the Bokeh.index - it needs to be unique
    b1 =  Button(label="START", width=100, css_classes=["b1_css"])
    on_click = CustomJS(code="""$.getJSON('{}', function(data) {{ }}); """.format(TEST_PORTAL_BUTTON + '?button={}'.format("b1")))
    b1.js_on_click(on_click)

    doc_layout.children.append(row(b1))

    _script, _div = components(doc_layout)
    d.body += raw(_script)
    d.body += raw(_div)
    return "{}".format(d)


@app.route(TEST_PORTAL_BUTTON, methods=['GET', 'POST'])
def url__test_button():
    logger.info("You Pressed: {}".format(request.args))
    d = {"success": True, "msg": None, "from": "url__test_button"}
    return jsonify(d)


@app.route(TEST_PORTAL_LISTENER, methods=['GET'])
def url__test_stream():
    def eventStream():
        while True:
            # wait for source data to be available, then push it
            yield """data: {}\n\n""".format(get_message())
    return Response(eventStream(), mimetype="text/event-stream")


def get_message():
    try:
        item = q.get(block=True)
        logger.info("forwarding {}".format(item["type"]))
        return item["type"]

    except queue.Empty:
        return "None"
    except Exception as e:
        logger.exception("Error processing event: %s" % e)


def send_message():
    count = 0
    while True:
        time.sleep(2)
        count += 1
        d = {"type": "b1 {}".format(count)}
        logger.info("Sending {}".format(d))
        q.put(d)


t = threading.Thread(target=send_message)
t.start()
app.run(host="0.0.0.0", port=6800, debug=False)

@Martin_Guthrie there may be a helper I forgot. If you get get the “top level” models, they all have select and select_one methods that can query the object graph:

So e.g. if you set a name property for your button, you could use those to query for an object with that name.

I see. I think I stumbled on adding my own properties to a Bokeh object… You are basically saying that I don’t need to use css_classes to hold a cookie to find the model… I can add any property name and use that, with select_one(myCookie)

b1 =  Button(label="START", width=100, myCookie="b1")

Is there a part of the object to run methods? For example I find a TextInput, and I want to make it in focus, fopr example, here obj is the object,

obj.model.focus();
obj.focus();

But focus() is not a method of either… I will print out the methods of everything to see if I can find it, and other methods.

You can’t add arbitrary properties to a Bokeh model (e.g. myCookie) but you can:

  • set a name property to a string value button.name="foo"
  • set a tags property to a list of arbitrary data button.tags=["foo", 10]

either of which you could write a predicate to search for using select. As for focus, those are methods on the actual DOM element, which are usually available on Bokeh views as view.el So, if you really need that, you may be better off searching Bokeh.index by hand, since that is the only way you will get ahold of Bokeh views (the views have a reference to their model, but models do not hold references to any views).

This is what worked for me on updating TextInput from Java,

            var el = getBokehModelByCSSClass(first(Bokeh.index), name);
            if (el) {{
                var title = params[0];
                var placeholder = params[1];                      
                el.model.visible = true;
                el.model.title = title;
                el.model.value = placeholder;
                el.input_el.select();
                el.input_el.focus();
                el.input_el.click();
            }} else {{
                console.log("ERROR: " + name + " Not found");
            }}

On Buttons, I can change the label, as shown in previous example, but backgroundColor is not being affected when I set it. I tried searching the object for other paths to backgroundColor and there are a few, but none of them affect the Button. Before using this Bokeh.index method, I used the following to change the Button label background color,

                el[0].style.backgroundColor = "#228B22";
                el[0].firstChild.style.backgroundColor = "#228B22";  

I will keep searching…

Regarding backgroundColor and other properties, I ended up using the css_classes feature, which programatically seems better. In Java, I do a $("." + name).removeClass("class1 class2 ..") and a $("." + name).addClass("class2") to get the changes I want. Where, class2 looks like,

{'button': {'background-color': "{}".format("#ABCDE")}}

Right, I could have been more clear: obtaining the Bokeh model the best way to change/update the explicitly defined Bokeh model properties, e.g. button.label. But there are not Bokeh model properties for every conceivable configurable aspect of underlying DOM elements. In cases where you want to update those (e.g. backgroundColor in this case) you will need to target CSS as you are doing.